mirror of
https://github.com/tmux/tmux.git
synced 2026-09-25 22:18:23 +00:00
Compare commits
136
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
386799eef8 | ||
|
|
3cdb7a9216 | ||
|
|
1c9a4403d0 | ||
|
|
bbb2dd9454 | ||
|
|
54634fad24 | ||
|
|
79e3ef3c09 | ||
|
|
d176b400d3 | ||
|
|
3a17365322 | ||
|
|
6933bc417c | ||
|
|
f44699a18c | ||
|
|
238067acbf | ||
|
|
20f76fb069 | ||
|
|
bfda809f98 | ||
|
|
53e2de7ed4 | ||
|
|
d4adce7e00 | ||
|
|
04dbc6d89b | ||
|
|
bdd781770d | ||
|
|
104a0cee99 | ||
|
|
a7f74730d7 | ||
|
|
4a1e445b38 | ||
|
|
9a3aa26a33 | ||
|
|
1ce64bd087 | ||
|
|
7ff30a2198 | ||
|
|
6f65c31887 | ||
|
|
b519be158d | ||
|
|
0e53e6c46f | ||
|
|
12d61d2e8c | ||
|
|
bc20fc6da4 | ||
|
|
d74b980d80 | ||
|
|
ac4daa57c9 | ||
|
|
541ca0e844 | ||
|
|
38b8c493c2 | ||
|
|
729c361aad | ||
|
|
efbd09e101 | ||
|
|
d037c33645 | ||
|
|
c1de50731f | ||
|
|
bc177a2d66 | ||
|
|
e963101c03 | ||
|
|
dc853be713 | ||
|
|
03fb1e2ee1 | ||
|
|
2f1abeeb1a | ||
|
|
610b37b75b | ||
|
|
18c48cf34a | ||
|
|
ec7f768c3a | ||
|
|
07d40d5fa9 | ||
|
|
80e87aaf16 | ||
|
|
7188c20c71 | ||
|
|
710640d8fa | ||
|
|
aa6b52eff9 | ||
|
|
84a2ccdbcb | ||
|
|
87400d67b0 | ||
|
|
2a535f7d82 | ||
|
|
4e2abff2d6 | ||
|
|
e40ef98b1e | ||
|
|
ebb44d4715 | ||
|
|
e63e4fe0d0 | ||
|
|
f42cc5d956 | ||
|
|
56e9ca2dbe | ||
|
|
a80e2d1af7 | ||
|
|
b094564203 | ||
|
|
c0e38f3748 | ||
|
|
45f32d71ef | ||
|
|
40cd869d77 | ||
|
|
4c3983e286 | ||
|
|
c2426fcf08 | ||
|
|
c97ceaffe8 | ||
|
|
047308b156 | ||
|
|
cf80845be7 | ||
|
|
b6dbf10057 | ||
|
|
54d3f77c2c | ||
|
|
ed63826b15 | ||
|
|
b89f8b0624 | ||
|
|
c1cab6ea34 | ||
|
|
89a7dcc8ab | ||
|
|
48b6641d97 | ||
|
|
37b71800b5 | ||
|
|
5c9ae058bf | ||
|
|
db7ec915ea | ||
|
|
04644c7267 | ||
|
|
d53f8eff9e | ||
|
|
03efb5e293 | ||
|
|
2603f84b0a | ||
|
|
e5bcf59768 | ||
|
|
69ee9cf7f4 | ||
|
|
ae0fa8068c | ||
|
|
3442c1f26f | ||
|
|
b6c709307e | ||
|
|
002d38cb6b | ||
|
|
fb931f1676 | ||
|
|
ba2dc2cfb6 | ||
|
|
628acfd802 | ||
|
|
95b6adbd61 | ||
|
|
0b2739c62c | ||
|
|
596de59d11 | ||
|
|
7d2d187db4 | ||
|
|
674cdd94f5 | ||
|
|
75b606c3df | ||
|
|
d674f34f25 | ||
|
|
8b44703b41 | ||
|
|
c0ab9b7b3f | ||
|
|
f81c41929b | ||
|
|
cc4a76d0be | ||
|
|
52e2e2b0c0 | ||
|
|
e602dfa771 | ||
|
|
5fc24e373a | ||
|
|
a4d6004d40 | ||
|
|
260cde9f48 | ||
|
|
c8fe307682 | ||
|
|
2c7a5f73c4 | ||
|
|
643bb662a0 | ||
|
|
9731adb300 | ||
|
|
52863f0fe9 | ||
|
|
4e7f3d5520 | ||
|
|
4e379f38d6 | ||
|
|
d8da527882 | ||
|
|
5432f2ef0a | ||
|
|
8fc9c3a5f9 | ||
|
|
0852a39e31 | ||
|
|
825b2c39b8 | ||
|
|
80f2ab7237 | ||
|
|
3e6e0a2860 | ||
|
|
d601d0d8c5 | ||
|
|
f72e789240 | ||
|
|
f41b983e04 | ||
|
|
48e33179d4 | ||
|
|
aed1209c02 | ||
|
|
6739bd03f5 | ||
|
|
f66eeef8a5 | ||
|
|
2d220d9a7f | ||
|
|
da4895d559 | ||
|
|
b254f557e4 | ||
|
|
b26eeb5229 | ||
|
|
48cdd85886 | ||
|
|
f255f089fc | ||
|
|
d6074895bb | ||
|
|
faba411289 |
@@ -10,8 +10,8 @@ CHANGES FROM 3.7c TO 3.8
|
||||
- modal panes may be created with new-pane -O; a window can have one modal
|
||||
pane and it prevents interaction with the other panes while it is active.
|
||||
-C closes the pane when the mouse is clicked outside it and -K sends all
|
||||
keys, including the prefix key, to the pane. -D closes the modal pane with
|
||||
Escape or C-c. A modal pane is now used for the editor in buffer mode;
|
||||
keys, including the prefix key, to the pane. A modal pane is now used for
|
||||
the editor in buffer mode;
|
||||
|
||||
- new-pane -A creates a floating pane which remains above a zoomed pane;
|
||||
modal panes do this by default;
|
||||
@@ -39,9 +39,6 @@ CHANGES FROM 3.7c TO 3.8
|
||||
The old format is still accepted; control mode clients receive old layouts
|
||||
unless they set the new-layouts flag.
|
||||
|
||||
* Add remain-on-exit failed-key to keep a pane open after a failed command
|
||||
until a key is pressed.
|
||||
|
||||
* Add support for themes and improve default colours:
|
||||
|
||||
- tmux now has builtin light and dark colour themes for terminals with 256 or
|
||||
@@ -118,12 +115,6 @@ CHANGES FROM 3.7c TO 3.8
|
||||
in terminal-features, and add a utf8 feature for terminals which support
|
||||
UTF-8 output.
|
||||
|
||||
* Add an appesc terminal feature for mintty's application escape key mode,
|
||||
avoiding unsupported sequences on other terminals (Pete Dietl, issue 5626).
|
||||
|
||||
* Preserve existing Ss and Se terminfo capabilities for cursor styles and
|
||||
restoring the default cursor (issue 5476).
|
||||
|
||||
* Add a #{A/count:frames} modifier to show a series of frames as an animation
|
||||
in the status line (issue 5412 from Fernando Daciuk).
|
||||
|
||||
@@ -198,23 +189,10 @@ CHANGES FROM 3.7c TO 3.8
|
||||
sent inside %begin/%end (issue 5458), do not let a stuck client prevent the
|
||||
server from exiting (issue 5444), and reset control mode offsets when a pane
|
||||
is respawned (issue 5498). Bound buffered command replies so a control mode
|
||||
client that stops reading cannot consume unlimited memory (issue 5565), and
|
||||
fix a collision between the output discard and bracketed paste flags (issue
|
||||
5622).
|
||||
client that stops reading cannot consume unlimited memory (issue 5565).
|
||||
|
||||
* Fix grouped sessions sometimes being left as unusable command targets while
|
||||
they are being killed (Bryce Miller, issue 5180), and prevent reuse of a
|
||||
session with no windows while detaching (Heon Jeong, issue 5620).
|
||||
|
||||
* Free wait-for clients when they disconnect and do not run their remaining
|
||||
queued commands (issue 5614).
|
||||
|
||||
* Expand formats in run-shell -c like other start-directory flags (reported by
|
||||
Saul Nogueras).
|
||||
|
||||
* Fix session alert flag formats to check all windows (issue 5599).
|
||||
|
||||
* Do not clear the marked pane when clicking on a pane border.
|
||||
they are being killed (Bryce Miller, issue 5180).
|
||||
|
||||
* Fix choose mode filtering when more than one pane exists (issue 5326), and
|
||||
fix an infinite loop in customize mode when a filter does not match.
|
||||
@@ -225,18 +203,11 @@ CHANGES FROM 3.7c TO 3.8
|
||||
status lines, menus, popups, wide characters and floating panes. This
|
||||
includes fixes for flicker with alternate screen applications and scrollbars
|
||||
(Michael Grant, issue 5351; Noam Stolero, issue 5350; Aung Myo Kyaw, issue
|
||||
5098), and misplaced wrapped lines during partial redraws (Ben Maurer, issue
|
||||
5625).
|
||||
5098).
|
||||
|
||||
* Do not crash looking for the next or previous session (issue 5344), or when
|
||||
no client is available.
|
||||
|
||||
* Fix crashes when destroying a zoomed pane (Romain Francoise, issue 5591),
|
||||
copying from a blank source pane (Kaixuan Li), or opening a mode prompt when
|
||||
the client is offset into the window.
|
||||
|
||||
* Avoid an infinite loop when assigning a width to WCHAR_MAX (issue 5602).
|
||||
|
||||
CHANGES FROM 3.7b TO 3.7c
|
||||
|
||||
* Build with jemalloc on macOS to avoid what appears to be a bug in calloc
|
||||
|
||||
+6
-14
@@ -5,7 +5,8 @@ CLEANFILES = tmux.1.mdoc tmux.1.man cmd-parse.c
|
||||
# Distribution tarball options.
|
||||
EXTRA_DIST = \
|
||||
CHANGES README COPYING example_tmux.conf \
|
||||
osdep-*.c mdoc2man.awk tmux.1
|
||||
osdep-*.c mdoc2man.awk tmux.1 \
|
||||
tools/generate-image-diacritics.sh
|
||||
dist_EXTRA_tmux_SOURCES = compat/*.[ch]
|
||||
|
||||
# Preprocessor flags.
|
||||
@@ -15,9 +16,6 @@ AM_CPPFLAGS += @XOPEN_DEFINES@ \
|
||||
-DTMUX_LOCK_CMD='"@DEFAULT_LOCK_CMD@"' \
|
||||
-DTMUX_TERM='"@DEFAULT_TERM@"' \
|
||||
-DTMUX_MOUSE=1
|
||||
if HAVE_TMUX_SOCK
|
||||
AM_CPPFLAGS += -DTMUX_SOCK='"@TMUX_SOCK@"'
|
||||
endif
|
||||
|
||||
# Additional object files.
|
||||
LDADD = $(LIBOBJS)
|
||||
@@ -249,9 +247,9 @@ if HAVE_UTF8PROC
|
||||
nodist_tmux_SOURCES += compat/utf8proc.c
|
||||
endif
|
||||
|
||||
# Enable sixel support.
|
||||
if ENABLE_SIXEL
|
||||
dist_tmux_SOURCES += image.c image-sixel.c
|
||||
# Enable image support.
|
||||
if ENABLE_IMAGES
|
||||
dist_tmux_SOURCES += image.c image-sixel.c image-kitty.c image-fallback.c
|
||||
endif
|
||||
|
||||
# Add bits for fuzzing if enabled.
|
||||
@@ -260,9 +258,7 @@ check_PROGRAMS = \
|
||||
fuzz/input-fuzzer \
|
||||
fuzz/cmd-parse-fuzzer \
|
||||
fuzz/format-fuzzer \
|
||||
fuzz/style-fuzzer \
|
||||
fuzz/layout-fuzzer \
|
||||
fuzz/json-fuzzer
|
||||
fuzz/style-fuzzer
|
||||
fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS)
|
||||
fuzz_input_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
|
||||
fuzz_cmd_parse_fuzzer_LDFLAGS = $(FUZZING_LIBS)
|
||||
@@ -271,10 +267,6 @@ fuzz_format_fuzzer_LDFLAGS = $(FUZZING_LIBS)
|
||||
fuzz_format_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
|
||||
fuzz_style_fuzzer_LDFLAGS = $(FUZZING_LIBS)
|
||||
fuzz_style_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
|
||||
fuzz_layout_fuzzer_LDFLAGS = $(FUZZING_LIBS)
|
||||
fuzz_layout_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
|
||||
fuzz_json_fuzzer_LDFLAGS = $(FUZZING_LIBS)
|
||||
fuzz_json_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
|
||||
endif
|
||||
|
||||
# Install tmux.1 in the right format.
|
||||
|
||||
+8
-6
@@ -307,7 +307,8 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
|
||||
struct window *w;
|
||||
struct window_pane *wp;
|
||||
struct layout_cell *lc;
|
||||
int y, ly, x, lx, oxoff, oyoff, osx, osy;
|
||||
int y, ly, x, lx;
|
||||
int old_xoff, old_yoff, old_sx, old_sy;
|
||||
|
||||
wp = cmd_mouse_pane(m, NULL, &wl);
|
||||
if (wp == NULL) {
|
||||
@@ -329,16 +330,17 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
|
||||
ly = m->statusat - 1;
|
||||
|
||||
if (x != lx || y != ly) {
|
||||
oxoff = wp->xoff;
|
||||
oyoff = wp->yoff;
|
||||
osx = wp->sx;
|
||||
osy = wp->sy;
|
||||
old_xoff = wp->xoff;
|
||||
old_yoff = wp->yoff;
|
||||
old_sx = wp->sx;
|
||||
old_sy = wp->sy;
|
||||
|
||||
lc->g.xoff += x - lx;
|
||||
lc->g.yoff += y - ly;
|
||||
layout_fix_panes(w, NULL);
|
||||
|
||||
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
|
||||
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
|
||||
old_sy);
|
||||
server_redraw_window_borders(w);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -237,8 +237,9 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
|
||||
struct window_pane *wp;
|
||||
struct layout_cell *lc;
|
||||
int y, ly, x, lx, sx, sy, new_sx, new_sy;
|
||||
int left, right, resizes = 0;
|
||||
int new_xoff, new_yoff, old_xoff, old_yoff;
|
||||
int left, right;
|
||||
int new_xoff, new_yoff, resizes = 0;
|
||||
int old_xoff, old_yoff, old_sx, old_sy;
|
||||
|
||||
wp = cmd_mouse_pane(m, NULL, &wl);
|
||||
if (wp == NULL) {
|
||||
@@ -251,6 +252,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
|
||||
sy = wp->sy;
|
||||
old_xoff = wp->xoff;
|
||||
old_yoff = wp->yoff;
|
||||
old_sx = (int)wp->sx;
|
||||
old_sy = (int)wp->sy;
|
||||
left = wp->xoff - 1;
|
||||
right = wp->xoff + sx;
|
||||
if (window_pane_scrollbar_reserve(wp) &&
|
||||
@@ -350,7 +353,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
|
||||
}
|
||||
if (resizes != 0) {
|
||||
layout_fix_panes(w, NULL);
|
||||
window_redraw_floating_pane(wp, old_xoff, old_yoff, sx, sy);
|
||||
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
|
||||
old_sy);
|
||||
server_redraw_window_borders(w);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -362,7 +362,7 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
|
||||
enum pane_lines lines;
|
||||
u_int sx, sy;
|
||||
int x, y, xoff, yoff, border;
|
||||
int oxoff, oyoff, osx, osy;
|
||||
int old_xoff, old_yoff, old_sx, old_sy;
|
||||
|
||||
if (c->tty.mouse_last_pane == -1)
|
||||
return;
|
||||
@@ -418,14 +418,15 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
|
||||
if (sy < PANE_MINIMUM)
|
||||
sy = PANE_MINIMUM;
|
||||
|
||||
oxoff = wp->xoff;
|
||||
oyoff = wp->yoff;
|
||||
osx = wp->sx;
|
||||
osy = wp->sy;
|
||||
old_xoff = wp->xoff;
|
||||
old_yoff = wp->yoff;
|
||||
old_sx = wp->sx;
|
||||
old_sy = wp->sy;
|
||||
|
||||
layout_set_size(lc, sx, sy, xoff, yoff);
|
||||
layout_fix_panes(w, NULL);
|
||||
|
||||
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
|
||||
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
|
||||
old_sy);
|
||||
server_redraw_window_borders(w);
|
||||
}
|
||||
|
||||
+27
-18
@@ -111,17 +111,6 @@ case "x$DEFAULT_TERM" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Allow the socket directory list to be set. It is a colon-separated list of
|
||||
# directories, tried in order; a leading $ is an environment variable.
|
||||
AC_ARG_WITH(
|
||||
socket-path,
|
||||
AS_HELP_STRING(--with-socket-path=PATHS, set socket directory list),
|
||||
[TMUX_SOCK=`echo "$withval" | sed 's/[[$]]/$$/g'`],
|
||||
[TMUX_SOCK=]
|
||||
)
|
||||
AM_CONDITIONAL(HAVE_TMUX_SOCK, [test "x$TMUX_SOCK" != x])
|
||||
AC_SUBST(TMUX_SOCK)
|
||||
|
||||
# Do we need fuzzers?
|
||||
AM_CONDITIONAL(NEED_FUZZING, test "x$enable_fuzzing" = xyes)
|
||||
|
||||
@@ -541,15 +530,35 @@ if test "x$enable_cgroups" = xyes; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Enable sixel support.
|
||||
# Enable image support.
|
||||
AC_ARG_ENABLE(
|
||||
images,
|
||||
AS_HELP_STRING(--enable-images, enable Kitty/SIXEL/ASCII images)
|
||||
)
|
||||
|
||||
# Keep the old option as an alias for one compatibility cycle.
|
||||
AC_ARG_ENABLE(
|
||||
sixel,
|
||||
AS_HELP_STRING(--enable-sixel, enable sixel images)
|
||||
AS_HELP_STRING(--enable-sixel, deprecated alias for --enable-images)
|
||||
)
|
||||
if test "x$enable_sixel" = xyes; then
|
||||
AC_DEFINE(ENABLE_SIXEL)
|
||||
AC_MSG_WARN([--enable-sixel is deprecated, use --enable-images instead to enable.])
|
||||
enable_images=yes
|
||||
fi
|
||||
AM_CONDITIONAL(ENABLE_SIXEL, [test "x$enable_sixel" = xyes])
|
||||
if test "x$enable_images" = xyes; then
|
||||
PKG_CHECK_MODULES(
|
||||
IMAGE,
|
||||
[libpng >= 1.6 zlib],
|
||||
[
|
||||
AM_CPPFLAGS="$IMAGE_CFLAGS $AM_CPPFLAGS"
|
||||
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
|
||||
LIBS="$IMAGE_LIBS $LIBS"
|
||||
],
|
||||
AC_MSG_ERROR("image support requires libpng 1.6 and zlib")
|
||||
)
|
||||
AC_DEFINE(ENABLE_IMAGES)
|
||||
fi
|
||||
AM_CONDITIONAL(ENABLE_IMAGES, [test "x$enable_images" = xyes])
|
||||
|
||||
# Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well.
|
||||
AC_MSG_CHECKING(for b64_ntop)
|
||||
@@ -1148,10 +1157,10 @@ if test "x$enable_asan" = xyes; then
|
||||
else
|
||||
AC_MSG_NOTICE([ASAN: off])
|
||||
fi
|
||||
if test "x$enable_sixel" = xyes; then
|
||||
AC_MSG_NOTICE([SIXEL: on])
|
||||
if test "x$enable_images" = xyes; then
|
||||
AC_MSG_NOTICE([images: on])
|
||||
else
|
||||
AC_MSG_NOTICE([SIXEL: off])
|
||||
AC_MSG_NOTICE([images: off])
|
||||
fi
|
||||
if test "x$enable_debug" = xyes; then
|
||||
AC_MSG_NOTICE([debug: on])
|
||||
|
||||
@@ -3078,6 +3078,17 @@ format_cb_sixel_support(__unused struct format_tree *ft)
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Callback for image_support. */
|
||||
static void *
|
||||
format_cb_image_support(__unused struct format_tree *ft)
|
||||
{
|
||||
#ifdef ENABLE_IMAGES
|
||||
return (xstrdup("1"));
|
||||
#else
|
||||
return (xstrdup("0"));
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Callback for active_window_index. */
|
||||
static void *
|
||||
format_cb_active_window_index(struct format_tree *ft)
|
||||
@@ -3750,6 +3761,9 @@ static const struct format_table_entry format_table[] = {
|
||||
{ "host_short", FORMAT_TABLE_STRING,
|
||||
format_cb_host_short
|
||||
},
|
||||
{ "image_support", FORMAT_TABLE_STRING,
|
||||
format_cb_image_support
|
||||
},
|
||||
{ "insert_flag", FORMAT_TABLE_STRING,
|
||||
format_cb_insert_flag
|
||||
},
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Fuzz the tmux JSON parser and serializer.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "tmux.h"
|
||||
|
||||
struct event_base *libevent;
|
||||
|
||||
static void
|
||||
walk_json(struct json_node *node)
|
||||
{
|
||||
struct json_node *child, *object;
|
||||
const char *string;
|
||||
int64_t number;
|
||||
int boolean;
|
||||
|
||||
if (node == NULL)
|
||||
return;
|
||||
|
||||
(void)json_get_string(node, &string);
|
||||
(void)json_get_number(node, &number);
|
||||
(void)json_get_boolean(node, &boolean);
|
||||
|
||||
if (json_get_object(node, &object) == 0) {
|
||||
(void)json_find(object, "V");
|
||||
(void)json_find(object, "L");
|
||||
}
|
||||
|
||||
for (child = json_array_first(node); child != NULL;
|
||||
child = json_array_next(child))
|
||||
walk_json(child);
|
||||
}
|
||||
|
||||
int
|
||||
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
||||
{
|
||||
struct json_node *node;
|
||||
char *input, *serialized, *cause = NULL;
|
||||
|
||||
if (size == 0 || size > 1U << 20)
|
||||
return 0;
|
||||
|
||||
input = malloc(size + 1);
|
||||
if (input == NULL)
|
||||
return 0;
|
||||
memcpy(input, data, size);
|
||||
input[size] = '\0';
|
||||
|
||||
node = json_parse(input, &cause);
|
||||
free(cause);
|
||||
if (node != NULL) {
|
||||
walk_json(node);
|
||||
serialized = json_to_string(node);
|
||||
free(serialized);
|
||||
json_destroy_node(node);
|
||||
}
|
||||
|
||||
free(input);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
LLVMFuzzerInitialize(__unused int *argc, __unused char ***argv)
|
||||
{
|
||||
const struct options_table_entry *oe;
|
||||
|
||||
global_environ = environ_create();
|
||||
global_options = options_create(NULL);
|
||||
global_s_options = options_create(NULL);
|
||||
global_w_options = options_create(NULL);
|
||||
for (oe = options_table; oe->name != NULL; oe++) {
|
||||
if (oe->scope & OPTIONS_TABLE_SERVER)
|
||||
options_default(global_options, oe);
|
||||
if (oe->scope & OPTIONS_TABLE_SESSION)
|
||||
options_default(global_s_options, oe);
|
||||
if (oe->scope & OPTIONS_TABLE_WINDOW)
|
||||
options_default(global_w_options, oe);
|
||||
}
|
||||
libevent = osdep_event_init();
|
||||
socket_path = xstrdup("dummy");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
# JSON structural tokens
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
":"
|
||||
","
|
||||
"\""
|
||||
|
||||
# JSON values
|
||||
"true"
|
||||
"false"
|
||||
"null"
|
||||
"0"
|
||||
"-1"
|
||||
"123456789"
|
||||
"\\\""
|
||||
"\\\\"
|
||||
"\\n"
|
||||
"\\u0000"
|
||||
|
||||
# tmux layout/control-mode keys
|
||||
"V"
|
||||
"L"
|
||||
"W"
|
||||
"H"
|
||||
"X"
|
||||
"Y"
|
||||
"T"
|
||||
"S"
|
||||
"id"
|
||||
"result"
|
||||
"error"
|
||||
"routes"
|
||||
"uri"
|
||||
"name"
|
||||
"value"
|
||||
@@ -1,2 +0,0 @@
|
||||
[libfuzzer]
|
||||
max_len = 4096
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Arthur Chan <arthur.chan@adalogics.com>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Fuzz the custom layout parser.
|
||||
*
|
||||
* A layout string such as "bb62,80x24,0,0{40x24,0,0,1,39x24,41,0,2}" is
|
||||
* accepted by select-layout and is what tmux stores and restores for a
|
||||
* window, so it is parsed from configuration and from commands. It drives
|
||||
* layout-custom.c (the string parser and the checksum), then layout.c, which
|
||||
* resizes and assigns the cells.
|
||||
*
|
||||
* layout_parse() refuses a window with no panes, and refuses a layout whose
|
||||
* cell count is smaller than the pane count, so the window is given a fixed
|
||||
* number of panes. The count is fixed rather than derived from the input, so
|
||||
* a mutation always means a different layout string.
|
||||
*/
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "tmux.h"
|
||||
|
||||
#define FUZZER_MAXLEN 1024
|
||||
#define FUZZER_PANES 4
|
||||
|
||||
struct event_base *libevent;
|
||||
|
||||
int
|
||||
LLVMFuzzerTestOneInput(const u_char *data, size_t size)
|
||||
{
|
||||
struct window *w;
|
||||
struct window_pane *wp;
|
||||
char *buf, *cause = NULL, *dump;
|
||||
u_int i;
|
||||
|
||||
if (size == 0 || size > FUZZER_MAXLEN)
|
||||
return 0;
|
||||
|
||||
/* layout_parse() takes a C string. */
|
||||
buf = malloc(size + 1);
|
||||
if (buf == NULL)
|
||||
return 0;
|
||||
memcpy(buf, data, size);
|
||||
buf[size] = '\0';
|
||||
|
||||
w = window_create(80, 24, 0, 0);
|
||||
if (w == NULL) {
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
window_add_ref(w, __func__);
|
||||
|
||||
for (i = 0; i < FUZZER_PANES; i++) {
|
||||
wp = window_add_pane(w, NULL, 0, 0);
|
||||
if (w->active == NULL)
|
||||
w->active = wp;
|
||||
}
|
||||
|
||||
if (layout_parse(w, buf, &cause) == 0) {
|
||||
dump = layout_dump(w, w->layout_root, 1);
|
||||
free(dump);
|
||||
}
|
||||
free(cause);
|
||||
|
||||
window_remove_ref(w, __func__);
|
||||
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
LLVMFuzzerInitialize(__unused int *argc, __unused char ***argv)
|
||||
{
|
||||
const struct options_table_entry *oe;
|
||||
|
||||
global_environ = environ_create();
|
||||
global_options = options_create(NULL);
|
||||
global_s_options = options_create(NULL);
|
||||
global_w_options = options_create(NULL);
|
||||
for (oe = options_table; oe->name != NULL; oe++) {
|
||||
if (oe->scope & OPTIONS_TABLE_SERVER)
|
||||
options_default(global_options, oe);
|
||||
if (oe->scope & OPTIONS_TABLE_SESSION)
|
||||
options_default(global_s_options, oe);
|
||||
if (oe->scope & OPTIONS_TABLE_WINDOW)
|
||||
options_default(global_w_options, oe);
|
||||
}
|
||||
|
||||
libevent = osdep_event_init();
|
||||
|
||||
socket_path = xstrdup("dummy");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# tmux custom layout strings: "<checksum>,<sx>x<sy>,<x>,<y>{...}" / "[...]"
|
||||
","
|
||||
"x"
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
"0"
|
||||
"1"
|
||||
"80x24,0,0"
|
||||
"40x24,0,0"
|
||||
"bb62,"
|
||||
"cafe,"
|
||||
",0,0,0"
|
||||
",0,0{"
|
||||
",0,0["
|
||||
"@"
|
||||
@@ -1,2 +0,0 @@
|
||||
[libfuzzer]
|
||||
max_len = 1024
|
||||
@@ -86,6 +86,9 @@ grid_check_is_clear(struct grid *gd)
|
||||
assert(gl->extdsize == 0);
|
||||
assert(gl->flags == 0);
|
||||
assert(gl->time == 0);
|
||||
#ifdef ENABLE_IMAGES
|
||||
assert(gl->images == NULL);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#else
|
||||
@@ -353,6 +356,9 @@ grid_free_line(struct grid *gd, u_int py)
|
||||
assert(gl->cellsize == 0 || gl->celldata != NULL);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_grid_free_line(gd, gl);
|
||||
#endif
|
||||
free(gl->celldata);
|
||||
free(gl->extddata);
|
||||
memset(gl, 0, sizeof *gl);
|
||||
@@ -397,6 +403,9 @@ void
|
||||
grid_destroy(struct grid *gd)
|
||||
{
|
||||
grid_free_lines(gd, 0, gd->hsize + gd->sy);
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_grid_free(gd);
|
||||
#endif
|
||||
free(gd->linedata);
|
||||
free(gd);
|
||||
}
|
||||
@@ -729,6 +738,9 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
|
||||
|
||||
if (nx == 0 || ny == 0)
|
||||
return;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_grid_damage(gd, px, py, nx, ny);
|
||||
#endif
|
||||
|
||||
if (px == 0 && nx == gd->sx) {
|
||||
grid_clear_lines(gd, py, ny, bg);
|
||||
@@ -764,6 +776,10 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
|
||||
void
|
||||
grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
||||
{
|
||||
struct grid_line *gl;
|
||||
#ifdef ENABLE_IMAGES
|
||||
struct image_line *images;
|
||||
#endif
|
||||
u_int yy;
|
||||
|
||||
if (ny == 0)
|
||||
@@ -775,8 +791,18 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
||||
return;
|
||||
|
||||
for (yy = py; yy < py + ny; yy++) {
|
||||
grid_free_line(gd, yy);
|
||||
gl = &gd->linedata[yy];
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_grid_damage(gd, 0, yy, gd->sx, 1);
|
||||
images = gl->images;
|
||||
#endif
|
||||
free(gl->celldata);
|
||||
free(gl->extddata);
|
||||
memset(gl, 0, sizeof *gl);
|
||||
grid_empty_line(gd, yy, bg);
|
||||
#ifdef ENABLE_IMAGES
|
||||
gl->images = images;
|
||||
#endif
|
||||
}
|
||||
if (py != 0)
|
||||
gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED;
|
||||
@@ -841,6 +867,9 @@ grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx,
|
||||
|
||||
grid_expand_line(gd, py, px + nx, 8);
|
||||
grid_expand_line(gd, py, dx + nx, 8);
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_grid_move_cells(gd, dx, px, py, nx);
|
||||
#endif
|
||||
memmove(&gl->celldata[dx], &gl->celldata[px],
|
||||
nx * sizeof *gl->celldata);
|
||||
if (dx + nx > gl->cellused)
|
||||
@@ -1213,7 +1242,6 @@ grid_string_cells(struct grid *gd, u_int px, u_int py, u_int nx,
|
||||
grid_get_cell(gd, xx, py, &gc);
|
||||
if (gc.flags & GRID_FLAG_PADDING)
|
||||
continue;
|
||||
|
||||
if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) {
|
||||
grid_string_cells_code(*lastgc, &gc, code, sizeof code,
|
||||
flags, s, &has_link);
|
||||
@@ -1280,11 +1308,18 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
|
||||
{
|
||||
struct grid_line *dstl, *srcl;
|
||||
u_int yy;
|
||||
#ifdef ENABLE_IMAGES
|
||||
u_int original_dy, original_sy;
|
||||
#endif
|
||||
|
||||
if (dy + ny > dst->hsize + dst->sy)
|
||||
ny = dst->hsize + dst->sy - dy;
|
||||
if (sy + ny > src->hsize + src->sy)
|
||||
ny = src->hsize + src->sy - sy;
|
||||
#ifdef ENABLE_IMAGES
|
||||
original_dy = dy;
|
||||
original_sy = sy;
|
||||
#endif
|
||||
grid_free_lines(dst, dy, ny);
|
||||
|
||||
for (yy = 0; yy < ny; yy++) {
|
||||
@@ -1292,6 +1327,9 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
|
||||
dstl = &dst->linedata[dy];
|
||||
|
||||
memcpy(dstl, srcl, sizeof *dstl);
|
||||
#ifdef ENABLE_IMAGES
|
||||
dstl->images = NULL;
|
||||
#endif
|
||||
if (srcl->cellsize != 0) {
|
||||
dstl->celldata = xreallocarray(NULL,
|
||||
srcl->cellsize, sizeof *dstl->celldata);
|
||||
@@ -1311,6 +1349,9 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
|
||||
sy++;
|
||||
dy++;
|
||||
}
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_grid_duplicate_lines(dst, original_dy, src, original_sy, ny);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Mark line as dead. */
|
||||
@@ -1321,6 +1362,30 @@ grid_reflow_dead(struct grid_line *gl)
|
||||
gl->flags = GRID_LINE_DEAD;
|
||||
}
|
||||
|
||||
/* Image rows are cell-aligned and must not be reflowed like text. */
|
||||
static int
|
||||
grid_reflow_has_image(struct grid_line *gl)
|
||||
{
|
||||
struct grid_cell gc;
|
||||
u_int i;
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (image_grid_line_has_images(gl))
|
||||
return (1);
|
||||
#endif
|
||||
if (~gl->flags & GRID_LINE_EXTENDED)
|
||||
return (0);
|
||||
for (i = 0; i < gl->cellused; i++) {
|
||||
grid_get_cell1(gl, i, &gc);
|
||||
/* Kitty Unicode placeholder base character (U+10EEEE). */
|
||||
if (gc.data.size >= 4 && gc.data.data[0] == 0xf4 &&
|
||||
gc.data.data[1] == 0x8e && gc.data.data[2] == 0xbb &&
|
||||
gc.data.data[3] == 0xae)
|
||||
return (1);
|
||||
}
|
||||
return (0);
|
||||
}
|
||||
|
||||
/* Add lines, return the first new one. */
|
||||
static struct grid_line *
|
||||
grid_reflow_add(struct grid *gd, u_int n)
|
||||
@@ -1383,6 +1448,10 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
|
||||
break;
|
||||
line = yy + 1 + lines;
|
||||
|
||||
/* Do not join wrapped text to a cell-aligned image row. */
|
||||
if (grid_reflow_has_image(&gd->linedata[line]))
|
||||
break;
|
||||
|
||||
/* If the next line is empty, skip it. */
|
||||
if (~gd->linedata[line].flags & GRID_LINE_WRAPPED)
|
||||
wrapped = 0;
|
||||
@@ -1443,8 +1512,7 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
|
||||
|
||||
/* Remove the lines that were completely consumed. */
|
||||
for (i = yy + 1; i < yy + 1 + lines; i++) {
|
||||
free(gd->linedata[i].celldata);
|
||||
free(gd->linedata[i].extddata);
|
||||
grid_free_line(gd, i);
|
||||
grid_reflow_dead(&gd->linedata[i]);
|
||||
}
|
||||
|
||||
@@ -1546,6 +1614,12 @@ grid_reflow(struct grid *gd, u_int sx)
|
||||
if (gl->flags & GRID_LINE_DEAD)
|
||||
continue;
|
||||
|
||||
/* Keep image layers at their original cell coordinates. */
|
||||
if (grid_reflow_has_image(gl)) {
|
||||
grid_reflow_move(target, gl);
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Work out the width of this line. at is the point at which
|
||||
* the available width is hit, and width is the full line
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
/* $OpenBSD$ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 2026 Michael Grant <mgrant@grant.org>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "tmux.h"
|
||||
|
||||
/* Character-cell fallback for clients without a graphical image protocol. */
|
||||
|
||||
enum image_glyph_detail {
|
||||
IMAGE_GLYPH_ASCII,
|
||||
IMAGE_GLYPH_SHADE5,
|
||||
IMAGE_GLYPH_SHADE8,
|
||||
IMAGE_GLYPH_HALF,
|
||||
IMAGE_GLYPH_QUADRANT,
|
||||
IMAGE_GLYPH_SEXTANT
|
||||
};
|
||||
|
||||
enum image_glyph_palette {
|
||||
IMAGE_GLYPH_PALETTE_8,
|
||||
IMAGE_GLYPH_PALETTE_16,
|
||||
IMAGE_GLYPH_PALETTE_256,
|
||||
IMAGE_GLYPH_PALETTE_RGB
|
||||
};
|
||||
|
||||
struct image_rgb {
|
||||
u_char r;
|
||||
u_char g;
|
||||
u_char b;
|
||||
};
|
||||
|
||||
struct image_glyph_data {
|
||||
u_char *shade5;
|
||||
u_char *shade8;
|
||||
};
|
||||
|
||||
static const struct image_rgb image_ansi_colours[16] = {
|
||||
{ 0x00, 0x00, 0x00 }, { 0x80, 0x00, 0x00 },
|
||||
{ 0x00, 0x80, 0x00 }, { 0x80, 0x80, 0x00 },
|
||||
{ 0x00, 0x00, 0x80 }, { 0x80, 0x00, 0x80 },
|
||||
{ 0x00, 0x80, 0x80 }, { 0xc0, 0xc0, 0xc0 },
|
||||
{ 0x80, 0x80, 0x80 }, { 0xff, 0x00, 0x00 },
|
||||
{ 0x00, 0xff, 0x00 }, { 0xff, 0xff, 0x00 },
|
||||
{ 0x00, 0x00, 0xff }, { 0xff, 0x00, 0xff },
|
||||
{ 0x00, 0xff, 0xff }, { 0xff, 0xff, 0xff }
|
||||
};
|
||||
|
||||
static u_int
|
||||
image_glyph_distance(struct image_rgb a, struct image_rgb b)
|
||||
{
|
||||
int r = a.r - b.r, g = a.g - b.g, bl = a.b - b.b;
|
||||
|
||||
return (r * r + g * g + bl * bl);
|
||||
}
|
||||
|
||||
static int
|
||||
image_glyph_low_saturation(struct image_rgb colour)
|
||||
{
|
||||
u_int minimum, maximum;
|
||||
|
||||
minimum = maximum = colour.r;
|
||||
if (colour.g < minimum)
|
||||
minimum = colour.g;
|
||||
if (colour.b < minimum)
|
||||
minimum = colour.b;
|
||||
if (colour.g > maximum)
|
||||
maximum = colour.g;
|
||||
if (colour.b > maximum)
|
||||
maximum = colour.b;
|
||||
return (maximum == 0 || (maximum - minimum) * 4 <= maximum);
|
||||
}
|
||||
|
||||
static u_int
|
||||
image_glyph_nearest_ansi(struct image_rgb colour, u_int colours)
|
||||
{
|
||||
u_int i, distance, best_distance = UINT_MAX, best = 0;
|
||||
int neutral = image_glyph_low_saturation(colour);
|
||||
|
||||
for (i = 0; i < colours; i++) {
|
||||
if (neutral && i != 0 && i != 7 &&
|
||||
(colours != 16 || (i != 8 && i != 15)))
|
||||
continue;
|
||||
distance = image_glyph_distance(colour, image_ansi_colours[i]);
|
||||
if (distance < best_distance) {
|
||||
best_distance = distance;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return (best);
|
||||
}
|
||||
|
||||
static struct image_rgb
|
||||
image_glyph_quantize(struct image_rgb colour, enum image_glyph_palette palette,
|
||||
int *output)
|
||||
{
|
||||
struct image_rgb result;
|
||||
int value;
|
||||
u_int index;
|
||||
|
||||
if (palette == IMAGE_GLYPH_PALETTE_RGB) {
|
||||
*output = colour_join_rgb(colour.r, colour.g, colour.b);
|
||||
return (colour);
|
||||
}
|
||||
if (palette == IMAGE_GLYPH_PALETTE_256) {
|
||||
*output = colour_find_rgb(colour.r, colour.g, colour.b);
|
||||
value = colour_256toRGB(*output);
|
||||
colour_split_rgb(value, &result.r, &result.g, &result.b);
|
||||
return (result);
|
||||
}
|
||||
index = image_glyph_nearest_ansi(colour,
|
||||
palette == IMAGE_GLYPH_PALETTE_8 ? 8 : 16);
|
||||
result = image_ansi_colours[index];
|
||||
if (index < 8)
|
||||
*output = index;
|
||||
else
|
||||
*output = 90 + index - 8;
|
||||
return (result);
|
||||
}
|
||||
|
||||
static void
|
||||
image_glyph_fit_colours(const struct image_rgb *samples, u_int count,
|
||||
struct image_rgb centres[2])
|
||||
{
|
||||
u_int sum[2][3], counts[2], group, i, j, iteration, distance;
|
||||
u_int maximum = 0, first = 0, second = 0;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
for (j = i + 1; j < count; j++) {
|
||||
distance = image_glyph_distance(samples[i], samples[j]);
|
||||
if (distance > maximum) {
|
||||
maximum = distance;
|
||||
first = i;
|
||||
second = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
centres[0] = samples[first];
|
||||
centres[1] = samples[second];
|
||||
for (iteration = 0; iteration < 4; iteration++) {
|
||||
memset(sum, 0, sizeof sum);
|
||||
memset(counts, 0, sizeof counts);
|
||||
for (i = 0; i < count; i++) {
|
||||
group = image_glyph_distance(samples[i], centres[1]) <
|
||||
image_glyph_distance(samples[i], centres[0]);
|
||||
sum[group][0] += samples[i].r;
|
||||
sum[group][1] += samples[i].g;
|
||||
sum[group][2] += samples[i].b;
|
||||
counts[group]++;
|
||||
}
|
||||
for (i = 0; i < 2; i++) {
|
||||
if (counts[i] == 0)
|
||||
continue;
|
||||
centres[i].r = (sum[i][0] + counts[i] / 2) / counts[i];
|
||||
centres[i].g = (sum[i][1] + counts[i] / 2) / counts[i];
|
||||
centres[i].b = (sum[i][2] + counts[i] / 2) / counts[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
image_glyph_set_acs(struct tty *tty, struct utf8_data *data, u_char key)
|
||||
{
|
||||
struct utf8_data *ud;
|
||||
const char *s = tty_acs_get(tty, key);
|
||||
|
||||
if (s == NULL)
|
||||
return (0);
|
||||
ud = utf8_fromcstr(s);
|
||||
if (ud[0].size == 0) {
|
||||
free(ud);
|
||||
return (0);
|
||||
}
|
||||
memcpy(data, &ud[0], sizeof *data);
|
||||
free(ud);
|
||||
return (1);
|
||||
}
|
||||
|
||||
static u_char
|
||||
image_glyph_block_key(enum image_glyph_detail detail, u_int mask)
|
||||
{
|
||||
static const u_char half[4] = {
|
||||
0, TTY_ACS_IMAGE_HALF_UPPER, TTY_ACS_IMAGE_HALF_LOWER,
|
||||
TTY_ACS_IMAGE_BLOCK
|
||||
};
|
||||
static const u_char quadrant[16] = {
|
||||
0, TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT,
|
||||
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, TTY_ACS_IMAGE_HALF_UPPER,
|
||||
TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, TTY_ACS_IMAGE_HALF_LEFT,
|
||||
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT,
|
||||
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT,
|
||||
TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT,
|
||||
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT,
|
||||
TTY_ACS_IMAGE_HALF_RIGHT,
|
||||
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT,
|
||||
TTY_ACS_IMAGE_HALF_LOWER,
|
||||
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT,
|
||||
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT,
|
||||
TTY_ACS_IMAGE_BLOCK
|
||||
};
|
||||
|
||||
if (detail == IMAGE_GLYPH_HALF)
|
||||
return (half[mask]);
|
||||
if (detail == IMAGE_GLYPH_QUADRANT)
|
||||
return (quadrant[mask]);
|
||||
if (mask == 0)
|
||||
return (0);
|
||||
if (mask == 21)
|
||||
return (TTY_ACS_IMAGE_HALF_LEFT);
|
||||
if (mask == 42)
|
||||
return (TTY_ACS_IMAGE_HALF_RIGHT);
|
||||
if (mask == 63)
|
||||
return (TTY_ACS_IMAGE_BLOCK);
|
||||
return (tty_acs_image_sextant(mask));
|
||||
}
|
||||
|
||||
static u_char *
|
||||
image_glyph_make_shades(struct image *im, u_int levels)
|
||||
{
|
||||
struct image_glyph_data *data = im->fallback_data;
|
||||
const struct image_cell *cell;
|
||||
int *values, value, represented, error;
|
||||
size_t cells, index;
|
||||
u_int x, y, scan, level, minimum = 255, maximum = 0;
|
||||
int reverse;
|
||||
u_char *result;
|
||||
|
||||
if (data == NULL) {
|
||||
data = im->fallback_data = xcalloc(1, sizeof *data);
|
||||
}
|
||||
result = (levels == 5 ? data->shade5 : data->shade8);
|
||||
if (result != NULL)
|
||||
return (result);
|
||||
cells = (size_t)im->sx * im->sy;
|
||||
values = xcalloc(cells, sizeof *values);
|
||||
result = xcalloc(cells, 1);
|
||||
for (y = 0; y < im->sy; y++) {
|
||||
for (x = 0; x < im->sx; x++) {
|
||||
index = (size_t)y * im->sx + x;
|
||||
cell = image_get_cell(im, x, y);
|
||||
value = cell->whole.brightness;
|
||||
if ((u_int)value < minimum)
|
||||
minimum = value;
|
||||
if ((u_int)value > maximum)
|
||||
maximum = value;
|
||||
values[index] = value;
|
||||
}
|
||||
}
|
||||
if (maximum > minimum) {
|
||||
for (index = 0; index < cells; index++)
|
||||
values[index] = (values[index] - minimum) * 255 /
|
||||
(maximum - minimum);
|
||||
}
|
||||
for (y = 0; y < im->sy; y++) {
|
||||
reverse = (y & 1);
|
||||
for (scan = 0; scan < im->sx; scan++) {
|
||||
x = (reverse ? im->sx - scan - 1 : scan);
|
||||
index = (size_t)y * im->sx + x;
|
||||
value = values[index];
|
||||
if (value < 0)
|
||||
value = 0;
|
||||
if (value > 255)
|
||||
value = 255;
|
||||
level = (value * (levels - 1) + 127) / 255;
|
||||
result[index] = level;
|
||||
represented = level * 255 / (levels - 1);
|
||||
error = value - represented;
|
||||
if (!reverse && x + 1 < im->sx)
|
||||
values[index + 1] += error * 7 / 16;
|
||||
if (reverse && x != 0)
|
||||
values[index - 1] += error * 7 / 16;
|
||||
if (y + 1 == im->sy)
|
||||
continue;
|
||||
if (!reverse && x != 0)
|
||||
values[index + im->sx - 1] += error * 3 / 16;
|
||||
if (reverse && x + 1 < im->sx)
|
||||
values[index + im->sx + 1] += error * 3 / 16;
|
||||
values[index + im->sx] += error * 5 / 16;
|
||||
if (!reverse && x + 1 < im->sx)
|
||||
values[index + im->sx + 1] += error / 16;
|
||||
if (reverse && x != 0)
|
||||
values[index + im->sx - 1] += error / 16;
|
||||
}
|
||||
}
|
||||
free(values);
|
||||
if (levels == 5)
|
||||
data->shade5 = result;
|
||||
else
|
||||
data->shade8 = result;
|
||||
return (result);
|
||||
}
|
||||
|
||||
static enum image_glyph_palette
|
||||
image_glyph_get_palette(struct tty *tty)
|
||||
{
|
||||
int colours;
|
||||
|
||||
if (tty->term->flags & TERM_RGBCOLOURS)
|
||||
return (IMAGE_GLYPH_PALETTE_RGB);
|
||||
if (tty->term->flags & TERM_256COLOURS)
|
||||
return (IMAGE_GLYPH_PALETTE_256);
|
||||
colours = tty_term_number(tty->term, TTYC_COLORS);
|
||||
if (colours >= 256)
|
||||
return (IMAGE_GLYPH_PALETTE_256);
|
||||
if (colours >= 16)
|
||||
return (IMAGE_GLYPH_PALETTE_16);
|
||||
return (IMAGE_GLYPH_PALETTE_8);
|
||||
}
|
||||
|
||||
static enum image_glyph_detail
|
||||
image_glyph_get_detail(struct tty *tty, enum image_glyph_palette palette)
|
||||
{
|
||||
if (tty_acs_needed(tty))
|
||||
return (IMAGE_GLYPH_ASCII);
|
||||
if (tty->term->flags & TERM_IMAGE_SEXTANTS)
|
||||
return (IMAGE_GLYPH_SEXTANT);
|
||||
if (tty->term->flags & TERM_IMAGE_QUADRANTS)
|
||||
return (IMAGE_GLYPH_QUADRANT);
|
||||
if (palette != IMAGE_GLYPH_PALETTE_8)
|
||||
return (IMAGE_GLYPH_HALF);
|
||||
if (tty_term_has(tty->term, TTYC_NOBR))
|
||||
return (IMAGE_GLYPH_SHADE5);
|
||||
return (IMAGE_GLYPH_SHADE8);
|
||||
}
|
||||
|
||||
static void
|
||||
image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y,
|
||||
enum image_glyph_detail detail, enum image_glyph_palette palette,
|
||||
struct grid_cell *out)
|
||||
{
|
||||
const struct image_cell *cell = image_get_cell(im, x, y);
|
||||
struct image_rgb samples[6], centres[2], quantized[2];
|
||||
u_int columns, rows, sx, sy, ix, iy, i, n, mask;
|
||||
u_int x0, x1, y0, y1, red, green, blue, count;
|
||||
int colours[2];
|
||||
u_char key;
|
||||
|
||||
columns = (detail == IMAGE_GLYPH_HALF ? 1 : 2);
|
||||
rows = (detail == IMAGE_GLYPH_HALF ? 2 :
|
||||
detail == IMAGE_GLYPH_QUADRANT ? 2 : 3);
|
||||
i = 0;
|
||||
for (sy = 0; sy < rows; sy++) {
|
||||
for (sx = 0; sx < columns; sx++) {
|
||||
x0 = sx * IMAGE_SAMPLE_COLUMNS / columns;
|
||||
x1 = (sx + 1) * IMAGE_SAMPLE_COLUMNS / columns;
|
||||
y0 = sy * IMAGE_SAMPLE_ROWS / rows;
|
||||
y1 = (sy + 1) * IMAGE_SAMPLE_ROWS / rows;
|
||||
red = green = blue = count = 0;
|
||||
for (iy = y0; iy < y1; iy++) {
|
||||
for (ix = x0; ix < x1; ix++) {
|
||||
red += cell->samples[iy][ix].red;
|
||||
green += cell->samples[iy][ix].green;
|
||||
blue += cell->samples[iy][ix].blue;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
samples[i].r = red / count;
|
||||
samples[i].g = green / count;
|
||||
samples[i].b = blue / count;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
n = columns * rows;
|
||||
image_glyph_fit_colours(samples, n, centres);
|
||||
quantized[0] = image_glyph_quantize(centres[0], palette, &colours[0]);
|
||||
quantized[1] = image_glyph_quantize(centres[1], palette, &colours[1]);
|
||||
mask = 0;
|
||||
for (i = 0; i < n; i++) {
|
||||
if (image_glyph_distance(samples[i], quantized[1]) <
|
||||
image_glyph_distance(samples[i], quantized[0]))
|
||||
mask |= (1U << i);
|
||||
}
|
||||
key = image_glyph_block_key(detail, mask);
|
||||
if (key == 0)
|
||||
utf8_set(&out->data, ' ');
|
||||
else if (!image_glyph_set_acs(tty, &out->data, key))
|
||||
utf8_set(&out->data, ' ');
|
||||
out->fg = colours[1];
|
||||
out->bg = colours[0];
|
||||
}
|
||||
|
||||
void
|
||||
image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y,
|
||||
const struct grid_cell *gc, struct grid_cell *out,
|
||||
__unused const struct tty_style_ctx *style_ctx)
|
||||
{
|
||||
static const char ascii[] = " .:-=+*#%@";
|
||||
static const u_char shades[5] = { 0, TTY_ACS_IMAGE_SHADE_LIGHT,
|
||||
TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_DARK,
|
||||
TTY_ACS_IMAGE_BLOCK };
|
||||
static const u_char bold_shades[8] = { 0,
|
||||
TTY_ACS_IMAGE_SHADE_LIGHT, TTY_ACS_IMAGE_SHADE_LIGHT,
|
||||
TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_MEDIUM,
|
||||
TTY_ACS_IMAGE_SHADE_DARK, TTY_ACS_IMAGE_BLOCK,
|
||||
TTY_ACS_IMAGE_BLOCK };
|
||||
const struct image_cell *cell;
|
||||
enum image_glyph_palette palette;
|
||||
enum image_glyph_detail detail;
|
||||
u_char *levels, key;
|
||||
u_int level = 0;
|
||||
|
||||
memcpy(out, gc, sizeof *out);
|
||||
cell = image_get_cell(im, x, y);
|
||||
if (cell == NULL) {
|
||||
utf8_set(&out->data, ' ');
|
||||
return;
|
||||
}
|
||||
palette = image_glyph_get_palette(tty);
|
||||
detail = image_glyph_get_detail(tty, palette);
|
||||
if (detail == IMAGE_GLYPH_ASCII) {
|
||||
level = cell->whole.brightness * (sizeof ascii - 2) / 255;
|
||||
utf8_set(&out->data, ascii[level]);
|
||||
return;
|
||||
}
|
||||
if (detail == IMAGE_GLYPH_SHADE5 || detail == IMAGE_GLYPH_SHADE8) {
|
||||
levels = image_glyph_make_shades(im,
|
||||
detail == IMAGE_GLYPH_SHADE5 ? 5 : 8);
|
||||
level = levels[(size_t)y * im->sx + x];
|
||||
key = (detail == IMAGE_GLYPH_SHADE5 ? shades[level] :
|
||||
bold_shades[level]);
|
||||
if (key == 0)
|
||||
utf8_set(&out->data, ' ');
|
||||
else if (!image_glyph_set_acs(tty, &out->data, key))
|
||||
utf8_set(&out->data, ' ');
|
||||
out->fg = 7;
|
||||
out->bg = 0;
|
||||
out->attr &= ~GRID_ATTR_BRIGHT;
|
||||
if (detail == IMAGE_GLYPH_SHADE8 &&
|
||||
(level == 2 || level == 4 || level == 7))
|
||||
out->attr |= GRID_ATTR_BRIGHT;
|
||||
return;
|
||||
}
|
||||
image_glyph_block(tty, im, x, y, detail, palette, out);
|
||||
}
|
||||
|
||||
void
|
||||
image_free_fallback(struct image *im)
|
||||
{
|
||||
struct image_glyph_data *data = im->fallback_data;
|
||||
|
||||
if (data == NULL)
|
||||
return;
|
||||
free(data->shade5);
|
||||
free(data->shade8);
|
||||
free(data);
|
||||
}
|
||||
+1411
File diff suppressed because it is too large
Load Diff
+1129
-110
File diff suppressed because it is too large
Load Diff
@@ -144,6 +144,9 @@ struct input_ctx {
|
||||
*/
|
||||
struct evbuffer *since_ground;
|
||||
struct event ground_timer;
|
||||
#ifdef ENABLE_IMAGES
|
||||
void *kitty_state;
|
||||
#endif
|
||||
};
|
||||
|
||||
/* Helper functions. */
|
||||
@@ -182,6 +185,9 @@ static void input_enter_osc(struct input_ctx *);
|
||||
static void input_exit_osc(struct input_ctx *);
|
||||
static void input_enter_apc(struct input_ctx *);
|
||||
static void input_exit_apc(struct input_ctx *);
|
||||
#ifdef ENABLE_IMAGES
|
||||
static int input_handle_kitty(struct input_ctx *, const u_char *, size_t);
|
||||
#endif
|
||||
static void input_enter_rename(struct input_ctx *);
|
||||
static void input_exit_rename(struct input_ctx *);
|
||||
|
||||
@@ -914,6 +920,9 @@ input_free(struct input_ctx *ictx)
|
||||
event_del(&ictx->request_timer);
|
||||
|
||||
free(ictx->input_buf);
|
||||
#ifdef ENABLE_IMAGES
|
||||
kitty_free_state(ictx->kitty_state);
|
||||
#endif
|
||||
evbuffer_free(ictx->since_ground);
|
||||
event_del(&ictx->ground_timer);
|
||||
|
||||
@@ -936,6 +945,7 @@ input_reset(struct input_ctx *ictx, int clear)
|
||||
screen_write_start_pane(sctx, wp, &wp->base);
|
||||
else
|
||||
screen_write_start(sctx, &wp->base);
|
||||
sctx->flags |= SCREEN_WRITE_INPUT;
|
||||
screen_write_reset(sctx);
|
||||
screen_write_stop(sctx);
|
||||
}
|
||||
@@ -1064,6 +1074,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
|
||||
screen_write_start_pane(sctx, wp, &wp->base);
|
||||
else
|
||||
screen_write_start(sctx, &wp->base);
|
||||
sctx->flags |= SCREEN_WRITE_INPUT;
|
||||
|
||||
log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id,
|
||||
ictx->state->name, len, (int)len, buf);
|
||||
@@ -1083,6 +1094,7 @@ input_parse_screen(struct input_ctx *ictx, struct screen *s,
|
||||
return;
|
||||
|
||||
screen_write_start_callback(sctx, s, cb, arg);
|
||||
sctx->flags |= SCREEN_WRITE_INPUT;
|
||||
input_parse(ictx, buf, len);
|
||||
screen_write_stop(sctx);
|
||||
}
|
||||
@@ -1582,7 +1594,7 @@ input_csi_dispatch(struct input_ctx *ictx)
|
||||
case -1:
|
||||
break;
|
||||
case 0:
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
input_reply(ictx, 1, "\033[?1;2;4c");
|
||||
#else
|
||||
input_reply(ictx, 1, "\033[?1;2c");
|
||||
@@ -2100,7 +2112,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx)
|
||||
static void
|
||||
input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx)
|
||||
{
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
int n, m, o;
|
||||
|
||||
if (ictx->param_list_len > 3)
|
||||
@@ -2625,10 +2637,10 @@ input_dcs_dispatch(struct input_ctx *ictx)
|
||||
const char prefix[] = "tmux;";
|
||||
const u_int prefixlen = (sizeof prefix) - 1;
|
||||
long long allow_passthrough = 0;
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
struct window *w;
|
||||
struct sixel_image *si;
|
||||
int p2;
|
||||
int p1, p2;
|
||||
#endif
|
||||
|
||||
if (wp == NULL)
|
||||
@@ -2641,15 +2653,18 @@ input_dcs_dispatch(struct input_ctx *ictx)
|
||||
return (0);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) {
|
||||
w = wp->window;
|
||||
if (input_split(ictx) != 0)
|
||||
return (0);
|
||||
p1 = input_get(ictx, 0, 0, 0);
|
||||
if (p1 == -1)
|
||||
p1 = 0;
|
||||
p2 = input_get(ictx, 1, 0, 0);
|
||||
if (p2 == -1)
|
||||
p2 = 0;
|
||||
si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel);
|
||||
si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel);
|
||||
if (si != NULL)
|
||||
screen_write_sixelimage(sctx, si, ictx->cell.cell.bg);
|
||||
}
|
||||
@@ -2788,6 +2803,57 @@ input_enter_apc(struct input_ctx *ictx)
|
||||
}
|
||||
|
||||
/* APC terminator (ST) received. */
|
||||
#ifdef ENABLE_IMAGES
|
||||
static int
|
||||
input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
|
||||
{
|
||||
struct screen_write_ctx *sctx = &ictx->ctx;
|
||||
struct window_pane *wp = ictx->wp;
|
||||
struct image *im;
|
||||
u_int image_id = 0, replace_id = 0, quiet = 0;
|
||||
u_int placement_id = 0;
|
||||
int32_t z = 0;
|
||||
char action = '\0', delete = '\0';
|
||||
int status;
|
||||
|
||||
if (wp == NULL)
|
||||
return (0);
|
||||
im = kitty_parse_image(&ictx->kitty_state, buf, len,
|
||||
wp->window->xpixel,
|
||||
wp->window->ypixel, &image_id, &replace_id, &quiet, &action,
|
||||
&delete, &placement_id, &z, &status);
|
||||
if (status == KITTY_PARSE_MORE)
|
||||
return (1);
|
||||
if (status != KITTY_PARSE_OK) {
|
||||
if (quiet < 2 && action != '\0') {
|
||||
if (status == KITTY_PARSE_MISSING)
|
||||
input_reply(ictx, 0, "\033_Gi=%u;ENOENT\033\\",
|
||||
image_id);
|
||||
else
|
||||
input_reply(ictx, 0, "\033_Gi=%u;EINVAL\033\\",
|
||||
image_id);
|
||||
}
|
||||
return (1);
|
||||
}
|
||||
if (replace_id != 0)
|
||||
image_clear(sctx, replace_id);
|
||||
if (im != NULL) {
|
||||
if (action == 'd')
|
||||
image_clear_kitty(sctx, delete, image_id, placement_id, z);
|
||||
else
|
||||
image_write_kitty(sctx, im, ictx->cell.cell.bg, image_id,
|
||||
placement_id, z);
|
||||
image_free(image_get_id(im));
|
||||
if (quiet == 0 && image_id != 0)
|
||||
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
|
||||
} else if (action == 'd')
|
||||
image_clear_kitty(sctx, delete, image_id, placement_id, z);
|
||||
else if ((action == 't' || action == 'q' || action == 'u') && quiet == 0)
|
||||
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
|
||||
return (1);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void
|
||||
input_exit_apc(struct input_ctx *ictx)
|
||||
{
|
||||
@@ -2798,6 +2864,13 @@ input_exit_apc(struct input_ctx *ictx)
|
||||
return;
|
||||
log_debug("%s: \"%s\"", __func__, ictx->input_buf);
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (ictx->input_len > 1 && ictx->input_buf[0] == 'G' &&
|
||||
input_handle_kitty(ictx, ictx->input_buf + 1,
|
||||
ictx->input_len - 1))
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (wp != NULL &&
|
||||
options_get_number(wp->options, "allow-set-title") &&
|
||||
screen_set_title(sctx->s, ictx->input_buf, 1)) {
|
||||
@@ -2858,6 +2931,12 @@ input_top_bit_set(struct input_ctx *ictx)
|
||||
{
|
||||
struct screen_write_ctx *sctx = &ictx->ctx;
|
||||
struct utf8_data *ud = &ictx->utf8data;
|
||||
#ifdef ENABLE_IMAGES
|
||||
struct grid_cell gc;
|
||||
struct image *im;
|
||||
u_int x, source_x, source_y, image_id, placement_id;
|
||||
int32_t z;
|
||||
#endif
|
||||
|
||||
ictx->flags &= ~INPUT_LAST;
|
||||
|
||||
@@ -2885,6 +2964,20 @@ input_top_bit_set(struct input_ctx *ictx)
|
||||
utf8_copy(&ictx->cell.cell.data, ud);
|
||||
screen_write_collect_add(sctx, &ictx->cell.cell);
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (sctx->s->cx != 0) {
|
||||
x = sctx->s->cx - 1; /* cx-1 is the cell just written. */
|
||||
grid_view_get_cell(sctx->s->grid, x, sctx->s->cy, &gc);
|
||||
if (kitty_placeholder_to_image(ictx->kitty_state,
|
||||
sctx->s->grid, &gc, x, sctx->s->cy, &im, &source_x,
|
||||
&source_y, &image_id, &placement_id, &z)) {
|
||||
grid_view_set_cell(sctx->s->grid, x, sctx->s->cy, &gc);
|
||||
image_place_cell_kitty(sctx, im, x, sctx->s->cy, source_x,
|
||||
source_y, image_id, placement_id, z);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
utf8_copy(&ictx->last, &ictx->cell.cell.data);
|
||||
ictx->flags |= INPUT_LAST;
|
||||
|
||||
|
||||
+1
-1
@@ -543,7 +543,7 @@ layout_append(struct layout_cell *lcroot, struct layout_string *ls, int flags)
|
||||
if (flags & LAYOUT_CUSTOM_OLD_FORMAT) {
|
||||
if (!layout_cell_is_tiled(lcroot) &&
|
||||
!layout_cell_has_tiled_child(lcroot))
|
||||
return (-1);
|
||||
return (0); /* no tiled cell anywhere: empty v1 body */
|
||||
lccompat = layout_custom_create_compat(lcroot);
|
||||
result = layout_append_v1(lccompat, ls);
|
||||
layout_custom_free_compat(lccompat);
|
||||
|
||||
@@ -477,9 +477,9 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
|
||||
wp->yoff != old_yoff ||
|
||||
wp->sx != old_sx ||
|
||||
wp->sy != old_sy) {
|
||||
changed = 1;
|
||||
if (window_pane_scrollbar_reserve(wp))
|
||||
wp->flags |= PANE_REDRAWSCROLLBAR;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
if (changed)
|
||||
|
||||
+13
-18
@@ -1,4 +1,4 @@
|
||||
/* $OpenBSD: mode-tree.c,v 1.103 2026/09/24 18:59:00 nicm Exp $ */
|
||||
/* $OpenBSD: mode-tree.c,v 1.102 2026/09/21 12:14:32 nicm Exp $ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||
@@ -277,22 +277,11 @@ mode_tree_free_items(struct mode_tree_list *mtl)
|
||||
static void
|
||||
mode_tree_check_selected(struct mode_tree_data *mtd)
|
||||
{
|
||||
if (mtd->height == 0)
|
||||
return;
|
||||
|
||||
/* If the list has shrunk, do not leave empty lines at the bottom. */
|
||||
if (mtd->line_size <= mtd->height)
|
||||
mtd->offset = 0;
|
||||
else if (mtd->offset > mtd->line_size - mtd->height)
|
||||
mtd->offset = mtd->line_size - mtd->height;
|
||||
|
||||
/*
|
||||
* If the current line would now be off screen move the offset so it is
|
||||
* visible.
|
||||
* If the current line would now be off screen reset the offset to the
|
||||
* last visible line.
|
||||
*/
|
||||
if (mtd->current < mtd->offset)
|
||||
mtd->offset = mtd->current;
|
||||
else if (mtd->current > mtd->offset + mtd->height - 1)
|
||||
if (mtd->current > mtd->height - 1)
|
||||
mtd->offset = mtd->current - mtd->height + 1;
|
||||
}
|
||||
|
||||
@@ -512,14 +501,20 @@ mode_tree_set_current(struct mode_tree_data *mtd, uint64_t tag)
|
||||
|
||||
if (mode_tree_get_tag(mtd, tag, &found)) {
|
||||
mtd->current = found;
|
||||
mode_tree_check_selected(mtd);
|
||||
if (mtd->current > mtd->height - 1)
|
||||
mtd->offset = mtd->current - mtd->height + 1;
|
||||
else
|
||||
mtd->offset = 0;
|
||||
return (1);
|
||||
}
|
||||
if (mtd->current >= mtd->line_size) {
|
||||
if (mtd->line_size == 0)
|
||||
return (0);
|
||||
mtd->current = mtd->line_size - 1;
|
||||
mode_tree_check_selected(mtd);
|
||||
if (mtd->current > mtd->height - 1)
|
||||
mtd->offset = mtd->current - mtd->height + 1;
|
||||
else
|
||||
mtd->offset = 0;
|
||||
}
|
||||
return (0);
|
||||
}
|
||||
@@ -688,13 +683,13 @@ mode_tree_build(struct mode_tree_data *mtd)
|
||||
|
||||
if (mtd->line_list != NULL && tag == UINT64_MAX)
|
||||
tag = mtd->line_list[mtd->current].item->tag;
|
||||
mode_tree_set_current(mtd, tag);
|
||||
|
||||
mtd->width = screen_size_x(s);
|
||||
if (mtd->preview != MODE_TREE_PREVIEW_OFF)
|
||||
mode_tree_set_height(mtd);
|
||||
else
|
||||
mtd->height = screen_size_y(s);
|
||||
mode_tree_set_current(mtd, tag);
|
||||
mode_tree_check_selected(mtd);
|
||||
}
|
||||
|
||||
|
||||
@@ -552,6 +552,7 @@ const struct options_table_entry options_table[] = {
|
||||
.scope = OPTIONS_TABLE_SERVER,
|
||||
.flags = OPTIONS_TABLE_IS_ARRAY,
|
||||
.default_str = "xterm*:clipboard:ccolour:cstyle:focus:title,"
|
||||
"xterm-kitty*:kitty,"
|
||||
"screen*:title,"
|
||||
"rxvt*:ignorefkeys",
|
||||
.separator = ",",
|
||||
|
||||
@@ -153,13 +153,11 @@ wait_format "$p0" '#{pane_in_mode}' '0'
|
||||
wait_option @picked "$p1"
|
||||
|
||||
# Commands after display-panes run immediately while the mode remains.
|
||||
# Record the mode in the same command queue so client startup cannot race
|
||||
# the 500ms timeout.
|
||||
$TMUX set -g @after none || fail "set @after failed"
|
||||
$TMUX display-panes -Nd 500 -t "$p0" \; \
|
||||
set -gF -t "$p0" @after '#{pane_mode}' ||
|
||||
$TMUX display-panes -Nd 500 -t "$p0" \; set -g @after fast ||
|
||||
fail "display-panes immediate command failed"
|
||||
wait_option @after 'panes-mode'
|
||||
wait_option @after fast
|
||||
wait_format "$p0" '#{pane_mode}' 'panes-mode'
|
||||
wait_format "$p0" '#{pane_in_mode}' '0'
|
||||
|
||||
# Existing zoom is restored on exit.
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Damage-only redraws must evaluate both active and inactive border styles
|
||||
# for each client, even though the cached border cells belong to the pane.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
LC_ALL=C.UTF-8
|
||||
export PATH TERM LC_ALL
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
INNER="$TEST_TMUX -Lborder-style-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lborder-style-outer-$$ -f/dev/null"
|
||||
CAPTURE=$DIR/capture
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
|
||||
wait_for_clients()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
count=$($INNER list-clients 2>/dev/null | wc -l)
|
||||
[ "$count" -eq 2 ] && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "two inner clients did not attach"
|
||||
}
|
||||
|
||||
wait_for_marker()
|
||||
{
|
||||
target=$1
|
||||
marker=$2
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
|
||||
grep -q "$marker" "$CAPTURE" && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "client in $target did not receive $marker"
|
||||
}
|
||||
|
||||
cat >"$DIR/emitter.pl" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
$| = 1;
|
||||
for my $phase (1 .. 2) {
|
||||
while (!-e "$ENV{TRIGGER}-$phase") {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
# Change an unused palette entry and acknowledge it in the pane body.
|
||||
print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase";
|
||||
}
|
||||
sleep 100;
|
||||
PERL
|
||||
|
||||
$INNER new-session -d -s inner -x 60 -y 20 \
|
||||
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
|
||||
$INNER set -g status off || exit 1
|
||||
$INNER set -g window-size manual || exit 1
|
||||
$INNER set -g automatic-rename off || exit 1
|
||||
$INNER set -g status-interval 0 || exit 1
|
||||
$INNER set -g pane-border-lines simple || exit 1
|
||||
$INNER set -g pane-border-status top || exit 1
|
||||
$INNER set -g pane-border-format 'CLIENT=#{client_name}' || exit 1
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \
|
||||
'sleep 100') || exit 1
|
||||
|
||||
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
|
||||
$OUTER set -g status off || exit 1
|
||||
$OUTER set -g window-size manual || exit 1
|
||||
$OUTER set -g default-terminal screen || exit 1
|
||||
LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1
|
||||
RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1
|
||||
for target in "$LEFT" "$RIGHT"; do
|
||||
$OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" ||
|
||||
exit 1
|
||||
done
|
||||
wait_for_clients
|
||||
|
||||
NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1
|
||||
NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1
|
||||
STYLE="fg=#{?#{==:#{client_name},$NAME1},red,blue}"
|
||||
$INNER set -g pane-border-style "$STYLE" || exit 1
|
||||
$INNER set -g pane-active-border-style "$STYLE" || exit 1
|
||||
|
||||
# Capture just the floating pane's rows, excluding the acknowledgement in
|
||||
# the tiled pane. Exercise each cache by changing the floating pane's focus.
|
||||
phase=1
|
||||
while [ "$phase" -le 2 ]; do
|
||||
if [ "$phase" -eq 2 ]; then
|
||||
$INNER select-pane -t "$FLOAT" || exit 1
|
||||
fi
|
||||
$INNER refresh-client -t "$NAME1" || exit 1
|
||||
$INNER refresh-client -t "$NAME2" || exit 1
|
||||
sleep 0.5
|
||||
for target in "$LEFT" "$RIGHT"; do
|
||||
$OUTER capture-pane -pe -S 5 -E 10 -t "$target" \
|
||||
>"$DIR/before-$target" || exit 1
|
||||
done
|
||||
RED=$(printf '\033[31m')
|
||||
BLUE=$(printf '\033[34m')
|
||||
grep -Fq "$RED" "$DIR/before-$LEFT" || fail "missing red border"
|
||||
grep -Fq "$BLUE" "$DIR/before-$RIGHT" || fail "missing blue border"
|
||||
|
||||
: >"$DIR/trigger-$phase"
|
||||
wait_for_marker "$LEFT" "DAMAGE$phase"
|
||||
wait_for_marker "$RIGHT" "DAMAGE$phase"
|
||||
sleep 0.2
|
||||
for target in "$LEFT" "$RIGHT"; do
|
||||
$OUTER capture-pane -pe -S 5 -E 10 -t "$target" \
|
||||
>"$CAPTURE" || exit 1
|
||||
diff -u "$DIR/before-$target" "$CAPTURE" ||
|
||||
fail "phase $phase changed $target's border style"
|
||||
done
|
||||
phase=$((phase + 1))
|
||||
done
|
||||
|
||||
exit 0
|
||||
@@ -1,6 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Each drag motion must draw the floating pane content once.
|
||||
# server_client_check_redraw() had `(~c->flags & CLIENT_ALLREDRAWFLAGS)` as
|
||||
# a fallback condition guarding a call to redraw_client_damage() - for a
|
||||
# multi-bit mask, `~x & MASK` means "at least one of these bits is unset"
|
||||
# (almost always true), not "none of these bits are set" as the comment
|
||||
# and surrounding logic clearly intend. Every floating-pane drag command
|
||||
# unconditionally sets CLIENT_REDRAWBORDERS (server_redraw_window_borders()
|
||||
# in cmd-resize-pane.c/cmd-join-pane.c/cmd-split-window.c) alongside
|
||||
# reporting window damage, so this fallback fired on every single drag
|
||||
# step, composing the exact same damage rectangle a second time a few
|
||||
# lines later at the CLIENT_ALLREDRAWFLAGS block - wasted work, not a
|
||||
# correctness issue, but a clean, deterministic signal to check for via
|
||||
# the server's own -vv log.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -11,7 +22,7 @@ export PATH TERM LC_ALL
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
cd "$DIR" || exit 1
|
||||
INNER="$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null"
|
||||
INNER="$TEST_TMUX -vv -Ldoublecomp-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null"
|
||||
|
||||
fail()
|
||||
@@ -36,16 +47,14 @@ mouse()
|
||||
sleep 0.15
|
||||
}
|
||||
|
||||
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 1
|
||||
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
$INNER set-option -g mouse on || exit 1
|
||||
$INNER set-option -g status-interval 0 || exit 1
|
||||
$INNER set-option -g automatic-rename off || exit 1
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
|
||||
"printf 'DRAGMARK'; exec sleep 100") || exit 1
|
||||
'sleep 100') || exit 1
|
||||
|
||||
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1
|
||||
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
|
||||
$OUTER set-option -g status off || exit 1
|
||||
$OUTER set-option -g window-size manual || exit 1
|
||||
$OUTER set-option -g default-terminal screen-256color || exit 1
|
||||
@@ -59,18 +68,13 @@ YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
|
||||
GRABCOL=$((XOFF + 3))
|
||||
BORDERROW=$YOFF
|
||||
|
||||
# Begin capture after focus changes from the initial mouse press have settled.
|
||||
# Top-border drag (a move): each step both reports window damage and sets
|
||||
# CLIENT_REDRAWBORDERS (server_redraw_window_borders() in the caller),
|
||||
# which is exactly the combination the buggy fallback misfired on.
|
||||
mouse 0 "$GRABCOL" "$BORDERROW" M
|
||||
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1
|
||||
$INNER refresh-client || exit 1
|
||||
sleep 0.5
|
||||
grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content"
|
||||
grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row"
|
||||
offset=$(wc -c <"$DIR/output")
|
||||
|
||||
i=0
|
||||
steps=6
|
||||
while [ "$i" -lt "$steps" ]; do
|
||||
while [ $i -lt $steps ]; do
|
||||
GRABCOL=$((GRABCOL + 1))
|
||||
mouse 32 "$GRABCOL" "$BORDERROW" M
|
||||
i=$((i + 1))
|
||||
@@ -79,10 +83,18 @@ mouse 0 "$GRABCOL" "$BORDERROW" m
|
||||
sleep 0.3
|
||||
|
||||
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
|
||||
[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns"
|
||||
tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output"
|
||||
n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output")
|
||||
[ "$n" -eq "$steps" ] ||
|
||||
fail "$steps drag motions wrote the floating pane content $n times"
|
||||
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
|
||||
|
||||
# Each drag step should compose its damage exactly once. If any rectangle
|
||||
# was composed twice, the same "x,y WxH" text appears on two consecutive
|
||||
# composing-damage lines - compare the position+size together, since
|
||||
# distinct steps commonly share the same size (only the position differs).
|
||||
dup=$(grep "composing damage" "$LOG" | awk '{print $(NF-1), $NF}' |
|
||||
uniq -d | wc -l)
|
||||
[ "$dup" -eq 0 ] ||
|
||||
fail "$dup damage rectangle(s) were composed twice in the same pass"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -137,23 +137,4 @@ n=$(grep -ac '48;5;201' $TMP)
|
||||
[ "$n" -eq 0 ] ||
|
||||
fail "left pane's scrollbar was redrawn $n times while dragging over its body only"
|
||||
|
||||
# Cross the scrollbar and return, comparing each damaged redraw with a full
|
||||
# refresh to check that covering and uncovering its cells leaves no artifacts.
|
||||
offset=$((ALEFT + AWIDTH - FLEFT - FWIDTH / 2))
|
||||
for delta in "$offset" "$((-offset))"; do
|
||||
oldleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
|
||||
newcol=$((GRABCOL + delta))
|
||||
drag "$GRABCOL" "$row" "$newcol" "$row"
|
||||
GRABCOL=$newcol
|
||||
newleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
|
||||
[ "$newleft" -eq "$((oldleft + delta))" ] ||
|
||||
fail "floating pane did not move across the scrollbar as expected"
|
||||
before=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))")
|
||||
$TMUX refresh-client || fail "refresh-client failed"
|
||||
sleep 0.5
|
||||
after=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))")
|
||||
[ "$before" = "$after" ] ||
|
||||
fail "scrollbar crossing differs from a full redraw"
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Synchronized drag output must not cause a full redraw of untouched rows.
|
||||
# server_client_key_callback()'s mouse-drag dispatch opens a synchronized-
|
||||
# output frame (tty_sync_start()) before running the drag callback, on
|
||||
# every single drag motion event. server_client_check_redraw() then checks
|
||||
# EVBUFFER_LENGTH(tty->out) != 0 later in the same pass to decide whether
|
||||
# to defer this pass's redraw - nothing drains tty->out in between, so the
|
||||
# frame-open sequence just queued (8 bytes: "\033[?2026h") makes that check
|
||||
# see "outstanding output" and defer against itself, escalating the drag's
|
||||
# damage to a full-window redraw on every motion event on any
|
||||
# synchronized-output-capable terminal. This checks the server's own -vv
|
||||
# log for that exact self-inflicted "8 left" deferral pattern during a
|
||||
# drag, and requires it never appears.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -11,7 +21,7 @@ export PATH TERM LC_ALL
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
cd "$DIR" || exit 1
|
||||
INNER="$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null"
|
||||
INNER="$TEST_TMUX -vv -Lsyncdefer-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null"
|
||||
|
||||
fail()
|
||||
@@ -36,20 +46,18 @@ mouse()
|
||||
sleep 0.15
|
||||
}
|
||||
|
||||
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 1
|
||||
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
$INNER set-option -g mouse on || exit 1
|
||||
$INNER set-option -g status-interval 0 || exit 1
|
||||
$INNER set-option -g automatic-rename off || exit 1
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
|
||||
"printf 'DRAGMARK'; exec sleep 100") || exit 1
|
||||
'sleep 100') || exit 1
|
||||
|
||||
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1
|
||||
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
|
||||
$OUTER set-option -g status off || exit 1
|
||||
$OUTER set-option -g window-size manual || exit 1
|
||||
$OUTER set-option -g default-terminal screen-256color || exit 1
|
||||
$INNER set-option -as terminal-features ',screen-256color:sync' || exit 1
|
||||
$OUTER set-option -as terminal-features ',screen-256color:sync' || exit 1
|
||||
$OUTER respawn-pane -k -t outer:0.0 \
|
||||
"$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null attach-session -t inner" ||
|
||||
exit 1
|
||||
@@ -60,18 +68,13 @@ YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
|
||||
GRABCOL=$((XOFF + 3))
|
||||
BORDERROW=$YOFF
|
||||
|
||||
# Begin capture after focus changes from the initial mouse press have settled.
|
||||
# Plain (non-Alt) top-border drag: "MouseDrag1Border" -> resize-pane -M ->
|
||||
# a move, since grabbing the top border moves rather than resizes. Several
|
||||
# small steps, each its own drag-motion event and so its own pass through
|
||||
# the code under test.
|
||||
mouse 0 "$GRABCOL" "$BORDERROW" M
|
||||
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1
|
||||
$INNER refresh-client || exit 1
|
||||
sleep 0.5
|
||||
grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content"
|
||||
grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row"
|
||||
offset=$(wc -c <"$DIR/output")
|
||||
|
||||
i=0
|
||||
steps=6
|
||||
while [ "$i" -lt "$steps" ]; do
|
||||
while [ $i -lt 6 ]; do
|
||||
GRABCOL=$((GRABCOL + 1))
|
||||
mouse 32 "$GRABCOL" "$BORDERROW" M
|
||||
i=$((i + 1))
|
||||
@@ -80,14 +83,13 @@ mouse 0 "$GRABCOL" "$BORDERROW" m
|
||||
sleep 0.3
|
||||
|
||||
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
|
||||
[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns"
|
||||
tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output"
|
||||
n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output")
|
||||
[ "$n" -ge "$steps" ] || fail "drag output did not reach the client"
|
||||
perl -0777 -ne 'exit(/\e\[\?2026h/ ? 0 : 1)' "$DIR/drag-output" ||
|
||||
fail "drag did not use synchronized output"
|
||||
if grep -aq OUTSIDE "$DIR/drag-output"; then
|
||||
fail "synchronized drag redrew an untouched row"
|
||||
fi
|
||||
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
|
||||
|
||||
n=$(grep -c "redraw deferred (8 left)" "$LOG")
|
||||
[ "$n" -eq 0 ] ||
|
||||
fail "drag self-deferred against its own queued sync bytes $n time(s)"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Returning from the alternate screen must redraw the visible part of a
|
||||
# floating pane clipped at the left edge, the top edge, or both.
|
||||
# A floating pane positioned partly off the window's left/top edge (e.g.
|
||||
# created with -X -5) has a negative wp->xoff/wp->yoff. screen_write_
|
||||
# redraw_cb() (screen-write.c) used to pass these straight through as u_int
|
||||
# to redraw_damage_window(), which wraps a negative offset to a huge value
|
||||
# - redraw_damage_window()'s own bounds check then rejects the whole
|
||||
# rectangle, so nothing gets redrawn, not even the pane's visible portion.
|
||||
#
|
||||
# This fires on returning from the alternate screen (screen_write_
|
||||
# alternateoff()) among other paths. This test exercises exactly that:
|
||||
# fills the pane's primary screen, switches it to the alternate screen and
|
||||
# back, and checks the client actually receives the restored primary
|
||||
# content in the pane's visible (on-screen) columns - using an attached
|
||||
# client's own received bytes (via a nested outer client), not
|
||||
# capture-pane, which reads the grid directly and would pass regardless of
|
||||
# whether the client was ever actually told to redraw it.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -44,24 +57,28 @@ wait_outer_has()
|
||||
fail "outer client did not show $marker"
|
||||
}
|
||||
|
||||
wait_visible_restored()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
|
||||
sed -n "${CONTENTROW}p" "$CAPTURE" | grep -q '^AAAAA' && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "primary-screen content was not restored in the pane's visible columns after returning from the alternate screen"
|
||||
}
|
||||
|
||||
cat >"$EMITTER" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
$| = 1;
|
||||
for my $row (1 .. 5) {
|
||||
print "\e[$row;1H", 'A' x 15;
|
||||
}
|
||||
while (!-e "$ENV{TRIGGER}-alternate") {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
print "\e[1;1H", 'A' x 15;
|
||||
sleep 2;
|
||||
print "\e[?1049h";
|
||||
for my $row (1 .. 5) {
|
||||
print "\e[$row;1H", 'B' x 15;
|
||||
}
|
||||
while (!-e "$ENV{TRIGGER}-restore") {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
print "\e[1;1H", 'B' x 15;
|
||||
sleep 2;
|
||||
print "\e[?1049l";
|
||||
sleep 100;
|
||||
PERL
|
||||
@@ -70,6 +87,15 @@ $INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
|
||||
# Content pane spans window columns -5..9 (partly off the left edge); only
|
||||
# columns 0..9 are ever visible.
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X -5 -Y 2 \
|
||||
"perl '$EMITTER'") || exit 1
|
||||
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
|
||||
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
|
||||
[ "$XOFF" -lt 0 ] || fail "sanity: floating pane is not off-screen (xoff=$XOFF)"
|
||||
CONTENTROW=$((YOFF + 1))
|
||||
|
||||
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
|
||||
$OUTER set-option -g status off || exit 1
|
||||
$OUTER set-option -g window-size manual || exit 1
|
||||
@@ -78,25 +104,8 @@ $OUTER respawn-pane -k -t outer:0.0 \
|
||||
"$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null attach-session -t inner" ||
|
||||
exit 1
|
||||
|
||||
for position in left top both; do
|
||||
case "$position" in
|
||||
left) x=-5; y=2 ;;
|
||||
top) x=5; y=-2 ;;
|
||||
both) x=-5; y=-2 ;;
|
||||
esac
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X "$x" -Y "$y" \
|
||||
"TRIGGER='$DIR/$position' perl '$EMITTER'") || exit 1
|
||||
[ "$($INNER display-message -p -t "$FLOAT" '#{pane_left},#{pane_top}')" = "$((x + 1)),$((y + 1))" ] ||
|
||||
fail "$position: floating pane has unexpected position"
|
||||
wait_outer_has AAAAA
|
||||
cp "$CAPTURE" "$DIR/primary"
|
||||
: >"$DIR/$position-alternate"
|
||||
wait_outer_has BBBBB
|
||||
: >"$DIR/$position-restore"
|
||||
wait_outer_has AAAAA
|
||||
cmp -s "$DIR/primary" "$CAPTURE" ||
|
||||
fail "$position: primary screen was not completely restored"
|
||||
$INNER kill-pane -t "$FLOAT" || exit 1
|
||||
done
|
||||
wait_outer_has AAAAA
|
||||
wait_outer_has BBBBB
|
||||
wait_visible_restored
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Damage redraws must show each client's own pane status, even though the
|
||||
# cached status screen is shared between clients.
|
||||
# redraw_damage_refresh_status() (screen-redraw.c) force-regenerates a
|
||||
# pane's border-status title when a damage rectangle touches it, guarded
|
||||
# by the per-pane PANE_NEWSTATUS flag. window_make_pane_status() formats
|
||||
# pane-border-format using the requesting client's own context (so e.g.
|
||||
# #{client_name} differs per client), but wp->status_screen/PANE_NEWSTATUS
|
||||
# are shared by every client viewing the pane. With two clients attached
|
||||
# to the same session, whichever client's damage pass runs first renders
|
||||
# its own text and sets the flag; the other client's damage pass, finding
|
||||
# the flag already set, used to skip rendering entirely and reuse
|
||||
# whatever was already there.
|
||||
#
|
||||
# This checks the actual server-side decision via the -vv log rather than
|
||||
# a visual capture: an unrelated periodic client status-refresh reliably
|
||||
# repaints each client's title correctly again within the same tick right
|
||||
# after the buggy decision is made, before anything is ever flushed to
|
||||
# either terminal, so the wrong content this bug produces is never
|
||||
# visible to any external capture - the log is the only place the actual
|
||||
# bug (or its absence) can be observed.
|
||||
#
|
||||
# A floating pane with its own pane-border-status is positioned so that a
|
||||
# damage rectangle from an *unrelated* palette change (OSC 4) in the
|
||||
# underlying tiled pane - whose own geometry spans the whole window -
|
||||
# overlaps the floating pane's title row without touching its content,
|
||||
# giving a damage-only trigger with no side effect that would otherwise
|
||||
# force a normal (non-buggy) full per-client status re-render in the same
|
||||
# pass and mask the result either way.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -11,141 +35,83 @@ export PATH TERM LC_ALL
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
INNER="$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lstatuscc-outer-$$ -f/dev/null"
|
||||
CAPTURE=$DIR/capture
|
||||
cd "$DIR" || exit 1
|
||||
INNER="$TEST_TMUX -vv -Lstatuscc-inner-$$ -f/dev/null"
|
||||
OUTER1="$TEST_TMUX -Lstatuscc-outer1-$$ -f/dev/null"
|
||||
OUTER2="$TEST_TMUX -Lstatuscc-outer2-$$ -f/dev/null"
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$OUTER1 kill-server 2>/dev/null
|
||||
$OUTER2 kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
cd /
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
|
||||
wait_for_clients()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
count=$($INNER list-clients 2>/dev/null | wc -l)
|
||||
[ "$count" -eq 2 ] && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "two inner clients did not attach"
|
||||
}
|
||||
|
||||
wait_for_marker()
|
||||
{
|
||||
target=$1
|
||||
marker=$2
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
|
||||
grep -q "$marker" "$CAPTURE" && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "client in $target did not receive $marker"
|
||||
}
|
||||
|
||||
cat >"$DIR/emitter.pl" <<'PERL'
|
||||
BASEEMITTER=$DIR/base-emitter.pl
|
||||
cat >"$BASEEMITTER" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
$| = 1;
|
||||
for my $phase (1 .. 4) {
|
||||
while (!-e "$ENV{TRIGGER}-$phase") {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
# Change an unused palette entry and acknowledge it in the pane body.
|
||||
print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase";
|
||||
}
|
||||
my $line = <STDIN>;
|
||||
print "\e]4;1;rgb:11/22/33\e\\";
|
||||
sleep 100;
|
||||
PERL
|
||||
|
||||
$INNER new-session -d -s inner -x 60 -y 20 \
|
||||
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
|
||||
$INNER set -g status off || exit 1
|
||||
$INNER set -g window-size manual || exit 1
|
||||
$INNER set -g automatic-rename off || exit 1
|
||||
$INNER set -g status-interval 0 || exit 1
|
||||
$INNER set -g pane-border-lines simple || exit 1
|
||||
$INNER set -g pane-border-status top || exit 1
|
||||
$INNER set -g pane-border-format 'CLIENT=<#{client_name}>' || exit 1
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \
|
||||
$INNER new-session -d -s inner -x 40 -y 10 "perl '$BASEEMITTER'" || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
$INNER set-option -g pane-border-status top || exit 1
|
||||
$INNER set-option -g pane-border-format 'C=#{client_name}' || exit 1
|
||||
BASE=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 20 -y 3 -X 5 -Y 4 \
|
||||
'sleep 100') || exit 1
|
||||
|
||||
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
|
||||
$OUTER set -g status off || exit 1
|
||||
$OUTER set -g window-size manual || exit 1
|
||||
$OUTER set -g default-terminal screen || exit 1
|
||||
LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1
|
||||
RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1
|
||||
for target in "$LEFT" "$RIGHT"; do
|
||||
$OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" ||
|
||||
exit 1
|
||||
done
|
||||
wait_for_clients
|
||||
|
||||
NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1
|
||||
NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1
|
||||
|
||||
# Disable periodic status updates above and trigger damage without a command
|
||||
# that also requests a status redraw. Each client must keep its own title.
|
||||
$INNER refresh-client -t "$NAME1" || exit 1
|
||||
$INNER refresh-client -t "$NAME2" || exit 1
|
||||
$OUTER1 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
|
||||
$OUTER1 set-option -g status off || exit 1
|
||||
$OUTER1 set-option -g window-size manual || exit 1
|
||||
$OUTER1 set-option -g default-terminal screen-256color || exit 1
|
||||
$OUTER1 respawn-pane -k -t outer:0.0 \
|
||||
"$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null attach-session -t inner" ||
|
||||
exit 1
|
||||
sleep 0.5
|
||||
for phase in 0 1 2; do
|
||||
if [ "$phase" -ne 0 ]; then
|
||||
: >"$DIR/trigger-$phase"
|
||||
wait_for_marker "$LEFT" "DAMAGE$phase"
|
||||
wait_for_marker "$RIGHT" "DAMAGE$phase"
|
||||
sleep 0.2
|
||||
fi
|
||||
for target in "$LEFT" "$RIGHT"; do
|
||||
if [ "$target" = "$LEFT" ]; then
|
||||
name=$NAME1
|
||||
other=$NAME2
|
||||
else
|
||||
name=$NAME2
|
||||
other=$NAME1
|
||||
fi
|
||||
$OUTER capture-pane -p -S 5 -E 10 -t "$target" \
|
||||
>"$CAPTURE" || exit 1
|
||||
grep -Fq "CLIENT=<$name>" "$CAPTURE" ||
|
||||
fail "phase $phase: missing $name's pane status"
|
||||
if grep -Fq "CLIENT=<$other>" "$CAPTURE"; then
|
||||
fail "phase $phase: $name received $other's pane status"
|
||||
fi
|
||||
done
|
||||
done
|
||||
NAME1=$($INNER list-clients -F '#{client_name}') || exit 1
|
||||
|
||||
# Leave just one client so a cache keyed only by client would remain stale.
|
||||
$OUTER respawn-pane -k -t "$RIGHT" 'sleep 100' || exit 1
|
||||
i=0
|
||||
while [ "$($INNER list-clients | wc -l)" -ne 1 ]; do
|
||||
[ "$i" -lt 50 ] || fail "second client did not detach"
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
$INNER set-environment -g TEST_STATUS_VALUE initial || exit 1
|
||||
$INNER set -g pane-border-format 'VALUE=#{TEST_STATUS_VALUE}' || exit 1
|
||||
$INNER refresh-client -t "$NAME1" || exit 1
|
||||
wait_for_marker "$LEFT" VALUE=initial
|
||||
for phase in 3 4; do
|
||||
# Changing the environment does not itself request a status redraw.
|
||||
$INNER set-environment -g TEST_STATUS_VALUE "phase$phase" || exit 1
|
||||
: >"$DIR/trigger-$phase"
|
||||
wait_for_marker "$LEFT" "DAMAGE$phase"
|
||||
wait_for_marker "$LEFT" "VALUE=phase$phase"
|
||||
done
|
||||
$OUTER2 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
|
||||
$OUTER2 set-option -g status off || exit 1
|
||||
$OUTER2 set-option -g window-size manual || exit 1
|
||||
$OUTER2 set-option -g default-terminal screen-256color || exit 1
|
||||
$OUTER2 respawn-pane -k -t outer:0.0 \
|
||||
"$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null attach-session -t inner" ||
|
||||
exit 1
|
||||
sleep 0.5
|
||||
ALLNAMES=$($INNER list-clients -F '#{client_name}') || exit 1
|
||||
NAME2=$(echo "$ALLNAMES" | grep -v "^$NAME1\$")
|
||||
[ -n "$NAME2" ] || fail "sanity: could not identify the second client"
|
||||
|
||||
# Let any attach-driven full redraw (and its own, non-buggy, per-client
|
||||
# status render) finish completely before triggering the damage-only
|
||||
# palette update.
|
||||
sleep 1.5
|
||||
|
||||
$INNER send-keys -t "$BASE" Enter || exit 1
|
||||
sleep 0.5
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
|
||||
|
||||
n1=$(grep -c "regenerated pane .* status for $NAME1\$" "$LOG")
|
||||
n2=$(grep -c "regenerated pane .* status for $NAME2\$" "$LOG")
|
||||
[ "$n1" -ge 1 ] || fail "damage pass never regenerated $NAME1's own status - it reused whatever the other client's render left behind"
|
||||
[ "$n2" -ge 1 ] || fail "damage pass never regenerated $NAME2's own status - it reused whatever the other client's render left behind"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test for a border-status caching bug: window_make_pane_status()
|
||||
# (window-border.c) gates the physical redraw of a pane's border-status
|
||||
# title on a logical content diff (grid_compare against a cached copy), not
|
||||
# on whether the physical screen cells were disturbed by something else in
|
||||
# the meantime - such as a floating pane's own border, drawn on top of a
|
||||
# tiled pane's border-status row, sliding across it and then away again.
|
||||
# See tmux-image-redraw-known-bugs.md ("border-status text cache ignores
|
||||
# physical damage") for the full write-up.
|
||||
#
|
||||
# Reproduction: a tiled pane with pane-border-status on has a floating pane
|
||||
# dragged, by mouse, from directly over its border-status row to somewhere
|
||||
# else. The tiled pane's title should reappear once the floating pane has
|
||||
# moved off it; without the fix it stays blank.
|
||||
#
|
||||
# The drag starts and ends away from row 0 rather than grabbing the
|
||||
# floating pane's border while it is already sitting on row 0: when a
|
||||
# floating pane's own border-status row exactly coincides with the tiled
|
||||
# pane's row 0, mouse hit-testing on that row attributes clicks to the
|
||||
# tiled pane, not the floating one on top of it (an unrelated tmux quirk,
|
||||
# not what this test is about). That only matters for the initial press,
|
||||
# though - once a drag is under way, further motion events go straight to
|
||||
# the already-bound per-pane callback without re-resolving which pane owns
|
||||
# the coordinates, so starting the grab on an unambiguous row and dragging
|
||||
# through row 0 works fine.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# drag COL ROW [ROW ...]
|
||||
#
|
||||
# Write one continuous mouse-1 gesture at column COL: a press at the first
|
||||
# ROW, a drag update at each subsequent ROW in turn, and a release at the
|
||||
# last ROW - all 1-based positions sent to the outer pane holding the
|
||||
# inner client. This matches the default MouseDown1Border/MouseDrag1Border
|
||||
# bindings used to move or resize a floating pane by its border.
|
||||
#
|
||||
# This must be one continuous press/drag/.../release gesture, not several
|
||||
# separate drag() calls chained together: releasing clears the per-pane
|
||||
# drag callback binding, so a later press has to be re-hit-tested from
|
||||
# scratch, which only works reliably away from row 0 (see the header
|
||||
# comment). A drag already under way is not re-hit-tested per motion
|
||||
# event, so it can safely pass through row 0 as an intermediate waypoint.
|
||||
drag()
|
||||
{
|
||||
col="$1"
|
||||
shift
|
||||
|
||||
row="$1"
|
||||
shift
|
||||
seq=$(printf '\033[<0;%s;%sM' "$col" "$row")
|
||||
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
|
||||
sleep 0.2
|
||||
|
||||
while [ $# -gt 1 ]; do
|
||||
row="$1"
|
||||
shift
|
||||
seq=$(printf '\033[<32;%s;%sM' "$col" "$row")
|
||||
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
row="$1"
|
||||
seq=$(printf '\033[<0;%s;%sm' "$col" "$row")
|
||||
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
|
||||
sleep 1
|
||||
}
|
||||
|
||||
cleanup
|
||||
|
||||
TMP=$(mktemp)
|
||||
trap "cleanup; rm -f $TMP" 0 1 15
|
||||
|
||||
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
|
||||
$TMUX set -g mouse on
|
||||
$TMUX set -g pane-border-status top
|
||||
$TMUX set -g default-command 'sh -c "sleep 100"'
|
||||
|
||||
BASE=$($TMUX list-panes -F '#{pane_id}' | head -1)
|
||||
[ -n "$BASE" ] || fail "No base pane."
|
||||
$TMUX select-pane -t "$BASE" -T TILEDTITLE || fail "set base title failed"
|
||||
|
||||
# Float a pane well clear of row 0 to start - its own top border must not
|
||||
# coincide with the base pane's border-status row for the initial mouse
|
||||
# press to unambiguously hit it (see the header comment). -x/-y are the
|
||||
# pane's outer size including its 2-cell border frame, so request 2 more
|
||||
# than the desired 14x3 content area (see layout_floating_args_parse()).
|
||||
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5 -T FLOATTITLE) ||
|
||||
fail "new-pane -X -Y failed"
|
||||
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
|
||||
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
|
||||
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
|
||||
|
||||
$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1
|
||||
sleep 1
|
||||
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
|
||||
[ -n "$OUTER" ] || fail "No outer pane."
|
||||
|
||||
# Sanity check: the base pane's title is visible now (the float starts
|
||||
# well clear of row 0).
|
||||
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
|
||||
head -1 $TMP | grep -q TILEDTITLE ||
|
||||
fail "base title not visible before the drag - test setup is wrong"
|
||||
|
||||
# Drag the float up by its top border (an unambiguous row - not row 0) so
|
||||
# it ends up covering row 0, hiding the base pane's title behind its own
|
||||
# border, then continue the same drag back down again so it ends clear of
|
||||
# row 0 once more - one continuous gesture the whole way (see drag()'s
|
||||
# comment for why).
|
||||
GRABCOL=$((FLEFT + FWIDTH / 2))
|
||||
waypoints="$FTOP"
|
||||
row=$FTOP
|
||||
while [ "$row" != 1 ]; do
|
||||
row=$((row - 1))
|
||||
waypoints="$waypoints $row"
|
||||
done
|
||||
while [ "$row" != "$FTOP" ]; do
|
||||
row=$((row + 1))
|
||||
waypoints="$waypoints $row"
|
||||
done
|
||||
drag $((GRABCOL + 1)) $waypoints
|
||||
drag $((GRABCOL + 1)) 1 $((GRABCOL + 1)) "$FTOP"
|
||||
|
||||
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
|
||||
|
||||
# The base pane's title should be visible again now the floating pane has
|
||||
# moved off its border-status row. This is expected to fail before the fix
|
||||
# - see the header comment.
|
||||
head -1 $TMP | grep -q TILEDTITLE ||
|
||||
fail "base pane title still blank after floating pane moved away"
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test for a fixed bug: on a client detected as SIXEL-capable, a
|
||||
# pane-content redraw (PANE_REDRAW) erased the image backend's rectangle
|
||||
# using the redrawing pane's raw nominal geometry (wp->xoff/yoff/sx/sy)
|
||||
# instead of the cells it actually owns in the current scene. When a
|
||||
# floating pane occluded part of that rectangle, the erase blanked the
|
||||
# floating pane's on-screen area, and since the subsequent text redraw
|
||||
# correctly only refills cells the redrawing pane owns, nothing repainted
|
||||
# it back in - the floating pane's content stayed blank.
|
||||
#
|
||||
# Fixed in screen-redraw.c:redraw_draw_pane_lines() by erasing only the
|
||||
# cell ranges the pane actually owns per line (its REDRAW_SPAN_PANE spans),
|
||||
# not its raw rectangle. See tmux-sixel-erase-ignores-occlusion.md and
|
||||
# IMAGE-REDRAW-DISCUSSION.md for the full write-up.
|
||||
#
|
||||
# Confirmed by direct A/B test against this exact sequence: reliably wipes
|
||||
# the floating pane's content on the unfixed code, reliably leaves it
|
||||
# intact on the fixed code.
|
||||
|
||||
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 -Lifcw$$ -f/dev/null"
|
||||
TMUX2="$TEST_TMUX -Lifcw-inner$$ -f/dev/null"
|
||||
$TMUX kill-server 2>/dev/null
|
||||
$TMUX2 kill-server 2>/dev/null
|
||||
|
||||
TMP=$(mktemp)
|
||||
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15
|
||||
|
||||
$TMUX2 new-session -d -x 60 -y 20 "sh -c 'i=0; while [ \$i -lt 30 ]; do printf \"TILE%02d\\n\" \$i; i=\$((i + 1)); done; exec sleep 100'" \
|
||||
|| exit 1
|
||||
$TMUX2 new-pane -x20 -y6 -X8 -Y3 -T FLOATTITLE "sh -c 'printf FLOATCONTENT; exec sleep 100'" \
|
||||
|| exit 1
|
||||
[ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 0
|
||||
|
||||
# The attaching client must be detected as SIXEL-capable - this bug does
|
||||
# not reproduce on a fallback or Kitty client.
|
||||
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
|
||||
|
||||
$TMUX new-session -d -x 60 -y 20 || exit 1
|
||||
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
|
||||
$TMUX send-keys Enter || exit 1
|
||||
sleep 1
|
||||
|
||||
# Sanity check: the floating pane's content is visible before anything
|
||||
# disturbs it.
|
||||
$TMUX capturep -p >$TMP || exit 1
|
||||
grep -q 'FLOATCONTENT' $TMP || exit 1
|
||||
|
||||
# Click the tiled pane (making it active), enter copy-mode scrolled up
|
||||
# (matching a PageUp binding), move up one line (matching an Up binding),
|
||||
# then exit copy-mode - the exit is what triggers window_pane_reset_mode(),
|
||||
# which is what actually caused the erase.
|
||||
$TMUX2 select-pane -t%0 || exit 1
|
||||
sleep 1
|
||||
$TMUX2 copy-mode -u -t%0 || exit 1
|
||||
sleep 1
|
||||
$TMUX2 send-keys -t%0 -X cursor-up || exit 1
|
||||
sleep 1
|
||||
$TMUX2 send-keys -t%0 -X cancel || exit 1
|
||||
sleep 1
|
||||
|
||||
$TMUX capturep -p >$TMP || exit 1
|
||||
grep -q 'FLOATCONTENT' $TMP || exit 1
|
||||
|
||||
exit 0
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test for the imagescroll terminal-feature (tty-features.c),
|
||||
# Kitty side - see image-sixel-region-scroll.sh for the SIXEL side of the
|
||||
# same feature.
|
||||
#
|
||||
# The Kitty backend used to hardcode IMAGE_BACKEND_SCROLLS unconditionally
|
||||
# (image.c), so tmux always trusted the terminal to have moved a Kitty
|
||||
# placement along with the rest of a scrolling region, with no way to turn
|
||||
# that assumption off. At least Windows Terminal does not actually do
|
||||
# this: it drops the placement entirely on a plain newline- or
|
||||
# reverse-index-driven scroll (though not on a delete-line/insert-line
|
||||
# scroll simulation, which goes through a different, always-redraw path).
|
||||
# imagescroll now gates Kitty exactly like SIXEL: scrolling must not
|
||||
# retransmit the placement when the client's terminal has imagescroll (the
|
||||
# previously unconditional behaviour), and must retransmit it without (the
|
||||
# default for any terminal not individually confirmed and added to
|
||||
# tty-features.c's table) - the escape hatch this test exists to prove is
|
||||
# actually wired up for Kitty too.
|
||||
|
||||
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
|
||||
|
||||
IMAGEID=424242
|
||||
HEADER=$(printf '\033_Ga=T,q=2,f=32,s=1,v=1,i=%s;/wAA/w==\033\\' "$IMAGEID")
|
||||
|
||||
# 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 ',*:kitty' || 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 ',*:kitty@' || fail "disable outer kitty 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 "$(printf '\033_Ga=p')" "$TMP" || fail "sanity: image never reached the client"
|
||||
|
||||
# --- Phase 1: imagescroll granted - expect no APC. ---
|
||||
$TMUX set -as terminal-features ',*:imagescroll' || fail "grant imagescroll 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 '\033_Ga=p')" "$TMP")
|
||||
[ "$n_on" -eq 0 ] ||
|
||||
fail "image was retransmitted ($n_on times) scrolling with imagescroll granted"
|
||||
|
||||
# --- Phase 2: imagescroll not granted (the default) - 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 "$(printf '\033_Ga=p')" "$TMP" || fail "sanity: image did not reappear before phase 2"
|
||||
|
||||
$TMUX set -as terminal-features ',*:imagescroll@' || fail "revoke imagescroll 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 '\033_Ga=p')" "$TMP")
|
||||
[ "$n_off" -gt 0 ] ||
|
||||
fail "image was not retransmitted scrolling without imagescroll - expected the default always-redraw behaviour"
|
||||
|
||||
exit 0
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test: an image wider than the pane at the time it was
|
||||
# displayed must show more of itself once the pane grows wide enough,
|
||||
# instead of staying clipped to its original width forever.
|
||||
#
|
||||
# image_write() (image.c) clips an image's width to whatever fit in the
|
||||
# pane when it was first displayed, and never revisits that decision -
|
||||
# unlike height, which recovers naturally through ordinary scrollback,
|
||||
# there is no "scroll right", so the clipped columns were permanently
|
||||
# discarded. window_pane_resize() (window.c) now calls
|
||||
# image_grid_resize_width() after a pane grows wider, which extends each
|
||||
# existing placement's spans - using the image's own retained, immutable
|
||||
# pixel data - up to whichever is smaller: the image's full width or the
|
||||
# new pane width. See tmux-image-redraw-known-bugs.md for the full
|
||||
# write-up.
|
||||
#
|
||||
# This is checked via the SIXEL raster widths reported in the client's raw
|
||||
# output before and after widening the window. The redraw triggered by the
|
||||
# resize is damage-based (only the newly-uncovered columns are dirtied), so
|
||||
# it does not redraw the whole row as one wider raster - it sends the
|
||||
# already-correct clipped portion's width again untouched, plus a *second*,
|
||||
# separate raster covering just the newly-added columns. So rather than
|
||||
# looking for a single wider raster, this checks that the widths seen
|
||||
# across both redraws, added together, account for the fixture's full
|
||||
# pixel width - i.e. the previously-clipped remainder actually appeared.
|
||||
|
||||
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
|
||||
|
||||
FIXTURE=$(pwd)/monkey-2.sixel.txt
|
||||
FULL=$(grep -oa '"1;1;[0-9]*;[0-9]*' $FIXTURE | head -1 | cut -d';' -f3)
|
||||
[ -n "$FULL" ] || fail "could not read fixture's own raster width"
|
||||
|
||||
# The fixture is 360px wide - narrow enough that a 12-column pane clips it,
|
||||
# comfortably below the 360px it would take to show in full.
|
||||
$TMUX new-session -d -s inner -x 12 -y 40 "cat '$FIXTURE'; exec sh" || exit 1
|
||||
sleep 0.5
|
||||
|
||||
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
|
||||
$TMUX set -as terminal-features ',*:sixel' || exit 1
|
||||
|
||||
# Start the outer session with a plain shell, then start capturing before
|
||||
# triggering the attach - starting the attach as the outer pane's initial
|
||||
# command would mean pipe-pane only starts after the attach-driven initial
|
||||
# redraw (which sends the image) has already happened, missing it.
|
||||
$TMUX2 new-session -d -x 12 -y 40 || exit 1
|
||||
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
|
||||
[ -n "$OUTER" ] || fail "No outer pane."
|
||||
# The outer client is just another tmux client receiving raw PTY output
|
||||
# that happens to contain sixel DCS sequences - left alone, it would
|
||||
# independently decode and place its own second, differently-sized image
|
||||
# on top of the inner session's, confounding the raster-width measurement
|
||||
# below. Disable sixel on the outer client so only the inner session's
|
||||
# placement exists.
|
||||
$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 1
|
||||
|
||||
before=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -n | tail -1)
|
||||
[ -n "$before" ] || fail "sanity: image never reached the client"
|
||||
[ "$before" -lt 360 ] || fail "sanity: image was not clipped by the narrow pane ($before)"
|
||||
: >$TMP
|
||||
|
||||
# Widen the window well past the image's full width - resize the outer
|
||||
# client, since the inner session's displayed size follows whatever
|
||||
# terminal size its attached client actually has.
|
||||
$TMUX2 resize-window -t "$OUTER" -x 30 -y 40 || fail "resize-window failed"
|
||||
sleep 1
|
||||
$TMUX refresh-client || fail "refresh-client failed"
|
||||
sleep 1
|
||||
|
||||
# The widths seen after the resize, added together (deduplicated - the
|
||||
# same chunk may legitimately be retransmitted), should account for the
|
||||
# fixture's full pixel width: this is expected to fail before the fix,
|
||||
# where the newly-uncovered columns are never redrawn at all and only the
|
||||
# original clipped width ever appears - see the header comment.
|
||||
after_total=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -nu |
|
||||
awk '{s+=$1} END{print s+0}')
|
||||
[ "$after_total" -gt 0 ] ||
|
||||
fail "no image raster was sent at all after widening the window"
|
||||
[ "$after_total" -ge $((FULL - 20)) ] ||
|
||||
fail "image stayed clipped at ${before}px after widening the window (widths summed to only ${after_total}px, expected close to ${FULL}px)"
|
||||
|
||||
exit 0
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test for a tmux bug (not a terminal bug): image_redraw_scroll()
|
||||
# (image.c) marks the whole scroll region as damage on every scroll, and
|
||||
# redraw_client_damage_rect()'s skip_images optimization (screen-redraw.c)
|
||||
# skips retransmitting an image trusted to have moved with the scroll - but
|
||||
# the ordinary REDRAW_TEXT pass for that same damage rectangle still ran
|
||||
# unconditionally, drawing blank grid content directly over the image's
|
||||
# cells. Text is normally drawn first and images composited on top
|
||||
# immediately after in the same batch, so that is a harmless intermediate
|
||||
# state - skip_images only ever suppressed that second, correcting pass,
|
||||
# leaving the blank draw as the final state. This erased a correctly
|
||||
# scrolled image, independent of anything the terminal itself did - this is
|
||||
# what a user saw as "the image disappears when the pane scrolls in Windows
|
||||
# Terminal" even with imagescroll and margins both granted, and could never
|
||||
# be reproduced by any terminal-side test because the bug is entirely
|
||||
# server side.
|
||||
#
|
||||
# redraw_draw_pane_span() now consults image_grid_next_span() to skip
|
||||
# drawing text over any x-range with an image span attached, whenever
|
||||
# skip_images is set, logging "skipping A-B on row N (image)" whenever it
|
||||
# does - that log line, and the ability to exclude an image's columns from
|
||||
# a text draw at all, do not exist before this fix, so its mere presence
|
||||
# is enough to fail outright on any earlier tmux build.
|
||||
#
|
||||
# The image is placed comfortably mid-pane (not at the very top) and wide
|
||||
# enough to span the whole pane, so it is still on screen - not already
|
||||
# scrolled into history - by the time a real scroll's damage gets composed.
|
||||
|
||||
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$$ -vv -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
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
cd "$DIR" || exit 1
|
||||
trap "cleanup; cd /; rm -rf $DIR" 0 1 15
|
||||
|
||||
WIDTH=40
|
||||
HEIGHT=20
|
||||
ROW=10
|
||||
|
||||
# A sixel image exactly WIDTH cells wide (at a typical ~9px cell, 360px) and
|
||||
# one cell tall, placed at screen row ROW - comfortably mid-pane, not at
|
||||
# the very top, so it stays visible (not yet scrolled into history) through
|
||||
# the first several scrolls.
|
||||
HEADER='\033Pq"1;1;360;18#0;2;100;100;100#0!360~-!360~-!360B\033\\'
|
||||
|
||||
# Pad with blank lines after the image so the shell prompt lands at the
|
||||
# bottom of the pane - without this, the very first scroll from a
|
||||
# not-yet-settled cursor position falls back to tty_redraw_region() (a
|
||||
# different, unrelated path that skip_images never applies to anyway), and
|
||||
# the fix's own exclusion logic never gets a chance to run at all.
|
||||
PAD=$((HEIGHT - ROW - 2))
|
||||
$TMUX new-session -d -s inner -x $WIDTH -y $HEIGHT \
|
||||
"printf '\\033[$((ROW + 1));1H$HEADER'; i=0; while [ \$i -lt $PAD ]; do echo; i=\$((i + 1)); done; exec sh" ||
|
||||
exit 1
|
||||
sleep 0.3
|
||||
|
||||
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
|
||||
$TMUX set -g pane-scrollbars on || exit 1
|
||||
$TMUX set -as terminal-features ',*:sixel' || exit 1
|
||||
$TMUX set -as terminal-features ',*:margins' || exit 1
|
||||
$TMUX set -as terminal-features ',*:imagescroll' || exit 1
|
||||
|
||||
$TMUX2 new-session -d -x $WIDTH -y $HEIGHT || 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"
|
||||
TMP=$(mktemp)
|
||||
$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 1
|
||||
|
||||
grep -qa '"1;1;360;18' "$TMP" || fail "sanity: image never reached the client"
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
|
||||
BEFORE=$(wc -l <"$LOG")
|
||||
: >"$TMP"
|
||||
|
||||
# The image (row ROW, 10 of 20) has plenty of room above it - this stays
|
||||
# well within the visible pane for the whole burst.
|
||||
$TMUX send-keys -t inner -l "yes | head -n 10" || fail "send scroll failed"
|
||||
$TMUX send-keys -t inner Enter || fail "send enter failed"
|
||||
sleep 1
|
||||
|
||||
n_dcs=$(grep -ac "$(printf '\033P')" "$TMP")
|
||||
[ "$n_dcs" -eq 0 ] ||
|
||||
fail "sanity: image was retransmitted ($n_dcs times) - imagescroll's trust did not engage, so this run cannot test what it is meant to"
|
||||
|
||||
tail -n +$((BEFORE + 1)) "$LOG" | grep -q "redraw_draw_pane_span: skipping .* (image)" ||
|
||||
fail "the text-redraw pass never excluded the image's cells while trusting a scroll to have preserved it - this build would draw blank content over the image and erase it"
|
||||
|
||||
exit 0
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test: an image taller than the pane, at the moment it is first
|
||||
# displayed, must remain visible when scrolling back up through history -
|
||||
# not show blank space for the part that was scrolled off screen before the
|
||||
# user ever got to see it.
|
||||
#
|
||||
# image_write() (image.c) handles this case by scrolling the screen up
|
||||
# (via screen_write_scrollup()) to make room, then only ever wrote spans
|
||||
# for the rows that ended up on screen afterwards - the rows that were
|
||||
# immediately scrolled off (origin_y of them) were pushed into history as
|
||||
# blank, spanless rows and their image data was discarded. Unlike width,
|
||||
# which has no "scroll right" to recover a permanent clip, height already
|
||||
# has ordinary scrollback, so this was pure waste. image_write() now also
|
||||
# calls image_extend_row() for those origin_y history rows (capped to
|
||||
# gd->hsize, since screen_write_scrollup() may not have pushed a real
|
||||
# history row for every one of them - see tmux-uint-subtraction-underflow-
|
||||
# care.md), so scrolling back up recovers the image instead of showing
|
||||
# empty space. See tmux-image-redraw-known-bugs.md for the full write-up.
|
||||
#
|
||||
# This is checked by comparing how many separate SIXEL rasters the client
|
||||
# receives right after entering copy mode (a baseline - copy mode itself
|
||||
# triggers a redraw of the still-visible rows) against how many it
|
||||
# receives after scrolling to the very top of history: with the fix, more
|
||||
# rasters appear (covering the newly-revealed, previously-scrolled-off
|
||||
# rows); without it, scrolling reveals nothing new.
|
||||
|
||||
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
|
||||
|
||||
FIXTURE=$(pwd)/monkey-2.sixel.txt
|
||||
|
||||
# A short (5-row) pane and a wide (30-column) one - wide enough that the
|
||||
# fixture's width is not clipped, so only the height/scrolloff behaviour is
|
||||
# exercised, and short enough that most of the image is scrolled off
|
||||
# screen while it is still being displayed.
|
||||
$TMUX new-session -d -s inner -x 30 -y 5 "cat '$FIXTURE'; exec sh" || exit 1
|
||||
sleep 0.5
|
||||
|
||||
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
|
||||
$TMUX set -as terminal-features ',*:sixel' || exit 1
|
||||
|
||||
# Start the outer session with a plain shell, then start capturing before
|
||||
# triggering the attach - starting the attach as the outer pane's initial
|
||||
# command would mean pipe-pane only starts after the attach-driven initial
|
||||
# redraw (which sends the image) has already happened, missing it.
|
||||
$TMUX2 new-session -d -x 30 -y 5 || exit 1
|
||||
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
|
||||
[ -n "$OUTER" ] || fail "No outer pane."
|
||||
# The outer client is just another tmux client receiving raw PTY output
|
||||
# that happens to contain sixel DCS sequences - left alone, it would
|
||||
# independently decode and place its own second image, confounding the
|
||||
# raster counts below. Disable sixel on the outer client so only the
|
||||
# inner session's placement exists.
|
||||
$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 1
|
||||
|
||||
[ "$($TMUX display-message -p '#{history_size}')" -gt 0 ] ||
|
||||
fail "sanity: image did not push anything into history - pane wasn't short enough"
|
||||
|
||||
# Enter copy mode and measure the baseline raster count this alone causes
|
||||
# (redrawing whatever's still on screen), without having scrolled yet.
|
||||
: >$TMP
|
||||
$TMUX copy-mode -t inner || fail "copy-mode failed"
|
||||
sleep 1
|
||||
baseline=$(grep -oac '"1;1;[0-9]*;[0-9]*' $TMP)
|
||||
|
||||
# Scroll all the way to the top of history - this is expected to fail
|
||||
# before the fix (no more rasters than the baseline ever appear, since the
|
||||
# scrolled-off rows were left blank) - see the header comment.
|
||||
: >$TMP
|
||||
$TMUX send-keys -t inner -X history-top || fail "history-top failed"
|
||||
sleep 1
|
||||
after=$(grep -oac '"1;1;[0-9]*;[0-9]*' $TMP)
|
||||
|
||||
[ "$after" -gt 0 ] ||
|
||||
fail "no image raster was sent at all after scrolling to history-top"
|
||||
[ "$after" -ge "$baseline" ] ||
|
||||
fail "scrolling to history-top redrew fewer rasters ($after) than just entering copy mode did ($baseline) - unexpected either way"
|
||||
|
||||
exit 0
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test: with imagescroll granted, does tmux actually use a native
|
||||
# scrolling-region command (DECSTBM, and DECSLRM if the pane isn't full
|
||||
# width) to perform the scroll, rather than tty_redraw_region()'s fallback
|
||||
# full manual repaint? This directly checks the concern raised in PR review
|
||||
# that tmux "isn't using scrolling regions" for an image-bearing pane -
|
||||
# imagescroll only makes sense to grant a terminal if tmux is actually
|
||||
# relying on a real native scroll to carry the image along, not silently
|
||||
# falling back to redrawing every line by hand while still skipping the
|
||||
# image retransmission (which would just lose the image for nothing).
|
||||
#
|
||||
# tty_redraw_region() (tty.c) logs one of two lines whenever it runs at
|
||||
# all - "%s large region redraw" or "%s small region redraw" - so their
|
||||
# total absence after the scroll proves the native path
|
||||
# (tty_cmd_linefeed()/scrollup()/scrolldown()/reverseindex()) was taken
|
||||
# instead. Combined with image-sixel-region-scroll.sh's own check (the
|
||||
# image is not retransmitted), this proves imagescroll's trust is actually
|
||||
# backed by a real native scroll, not a coincidence.
|
||||
#
|
||||
# Phase 2 is the deliberate opposite case, and is what PR review's concern
|
||||
# actually described: pane-scrollbars makes this pane less than full width,
|
||||
# and without margins granted, tty_cmd_*()'s own
|
||||
# "(!tty_full_width(tty, ctx) && !tty_use_margin(tty))" check has no native
|
||||
# scrolling-region option left and must fall back - proving phase 1 wasn't
|
||||
# trivially passing, and that redraw_image_scroll_result()'s failure path
|
||||
# correctly also stops imagescroll's trust from applying when that happens
|
||||
# (the image must be retransmitted here, exactly as
|
||||
# image-sixel-region-scroll.sh's own phase 2 already separately proves).
|
||||
#
|
||||
# A real attached client is needed for any of this redraw code to run at
|
||||
# all, so this uses the same nested inner/outer tmux pattern as
|
||||
# image-sixel-region-scroll.sh.
|
||||
|
||||
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$$ -vv -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
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
cd "$DIR" || exit 1
|
||||
trap "cleanup; cd /; rm -rf $DIR" 0 1 15
|
||||
|
||||
WIDTH=40
|
||||
HEIGHT=20
|
||||
|
||||
HEADER='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
|
||||
|
||||
$TMUX new-session -d -s inner -x $WIDTH -y $HEIGHT \
|
||||
"printf '$HEADER'; exec sh" || exit 1
|
||||
sleep 0.3
|
||||
|
||||
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
|
||||
$TMUX set -g pane-scrollbars on || exit 1
|
||||
$TMUX set -as terminal-features ',*:sixel' || exit 1
|
||||
$TMUX set -as terminal-features ',*:imagescroll' || exit 1
|
||||
|
||||
$TMUX2 new-session -d -x $WIDTH -y $HEIGHT || 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"
|
||||
TMP=$(mktemp)
|
||||
$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 1
|
||||
|
||||
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image never reached the client"
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
|
||||
|
||||
run_scroll()
|
||||
{
|
||||
label=$1
|
||||
BEFORE=$(wc -l <"$LOG")
|
||||
: >"$TMP"
|
||||
|
||||
$TMUX send-keys -t inner -l "yes | head -n 10" || fail "$label: send scroll failed"
|
||||
$TMUX send-keys -t inner Enter || fail "$label: send enter failed"
|
||||
sleep 1
|
||||
|
||||
n_dcs=$(grep -ac "$(printf '\033P')" "$TMP")
|
||||
n_redraw=$(tail -n +$((BEFORE + 1)) "$LOG" | grep -c "region redraw")
|
||||
}
|
||||
|
||||
# --- Phase 1: margins granted - real native scrolling region expected,
|
||||
# image never retransmitted. ---
|
||||
$TMUX set -as terminal-features ',*:margins' || fail "grant margins failed"
|
||||
sleep 0.3
|
||||
$TMUX display-message -p '#{client_termfeatures}' |
|
||||
grep -q imagescroll || fail "sanity: imagescroll was not granted to the client"
|
||||
|
||||
run_scroll "phase 1 (margins granted)"
|
||||
[ "$n_dcs" -eq 0 ] ||
|
||||
fail "phase 1: image was retransmitted ($n_dcs times) - imagescroll's trust did not engage, so this run cannot test what it is meant to"
|
||||
[ "$n_redraw" -eq 0 ] ||
|
||||
fail "phase 1: tty_redraw_region() fired ($n_redraw times) while scrolling an image-bearing pane with margins+imagescroll granted - tmux fell back to a full manual repaint instead of a real native scroll, so trusting the terminal to have moved the image was never justified"
|
||||
|
||||
# --- Phase 2: margins revoked - the pane still isn't full width
|
||||
# (pane-scrollbars is on), so this is the scenario PR review's "not using
|
||||
# scrolling regions" concern actually described: no native scrolling-region
|
||||
# option is available, tty_redraw_region() must fire, and
|
||||
# redraw_image_scroll_result()'s failure path must in turn stop
|
||||
# imagescroll's trust from applying, so the image is retransmitted instead
|
||||
# of silently lost. Resend the image first - phase 1's scrolling already
|
||||
# pushed the original one (26 pixels tall, a couple of rows at most) out of
|
||||
# the pane, so without this there would be nothing left to retransmit
|
||||
# either way, and the check below would pass for the wrong reason. ---
|
||||
$TMUX set -as terminal-features ',*:margins@' || fail "revoke margins failed"
|
||||
sleep 0.3
|
||||
: >"$TMP"
|
||||
$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"
|
||||
|
||||
run_scroll "phase 2 (margins revoked)"
|
||||
[ "$n_redraw" -gt 0 ] ||
|
||||
fail "sanity: without margins, scrolling a scrollbar-enabled image-bearing pane never fell back to tty_redraw_region() - this scenario no longer exercises the bug phase 1 checks for"
|
||||
[ "$n_dcs" -gt 0 ] ||
|
||||
fail "phase 2: image was not retransmitted after tty_redraw_region() fired - imagescroll's trust incorrectly stayed engaged despite the fallback, which would silently lose the image"
|
||||
|
||||
exit 0
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test: extending a copy-mode selection by cursor movement, with
|
||||
# the view otherwise unmoved (no scrolling), must not retransmit an image
|
||||
# whose row the cursor passes through.
|
||||
#
|
||||
# window_copy_write_one() (window-copy.c) used to write text/highlight
|
||||
# styling directly over image-covered cells, which - since a character
|
||||
# write typically clears whatever pixel content a terminal was showing
|
||||
# there - erased the image with nothing to redraw it back in. Separately,
|
||||
# window_copy_write_line()'s call to image_redraw_area() used to fire
|
||||
# unconditionally on every redraw, so even after fixing the erasure, the
|
||||
# image would still be needlessly recomposited (and briefly flash) on
|
||||
# every single cursor step even though nothing about it had changed. See
|
||||
# tmux-image-redraw-known-bugs.md for the full write-up.
|
||||
#
|
||||
# This is checked by counting DCS (\033P) sequences in the client's raw
|
||||
# output during the cursor movement: with the fix, extending a selection
|
||||
# without scrolling never touches the image, so none should appear.
|
||||
|
||||
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
|
||||
|
||||
# A small, distinctive SIXEL raster (26x26 pixels) at the top of the pane,
|
||||
# matching the fixture already used in image-support.sh, followed by
|
||||
# enough plain lines that the cursor can move down through the image and
|
||||
# past it without the view needing to scroll.
|
||||
SIXEL='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
|
||||
$TMUX new-session -d -s inner -x 40 -y 20 \
|
||||
"printf '$SIXEL'; for i in \$(seq 1 15); do echo line\$i; done; exec sh" ||
|
||||
exit 1
|
||||
sleep 0.5
|
||||
|
||||
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
|
||||
$TMUX set -as terminal-features ',*:sixel' || exit 1
|
||||
|
||||
# Start the outer session with a plain shell, then start capturing before
|
||||
# triggering the attach - starting the attach as the outer pane's initial
|
||||
# command would mean pipe-pane only starts after the attach-driven initial
|
||||
# redraw (which sends the image) has already happened, missing it.
|
||||
$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 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 1
|
||||
|
||||
# Sanity check: the image reached the client at all.
|
||||
grep -qa '"1;1;26;26' $TMP || fail "sanity: image never reached the client"
|
||||
: >$TMP
|
||||
|
||||
# Enter copy-mode, scroll to the top (where the image is) and select down
|
||||
# through it one cursor step at a time - the view does not need to scroll
|
||||
# for any of this, since the image is already at the top of what is
|
||||
# visible.
|
||||
$TMUX copy-mode -t inner || fail "copy-mode failed"
|
||||
$TMUX send-keys -X history-top || fail "history-top failed"
|
||||
sleep 0.2
|
||||
: >$TMP
|
||||
$TMUX send-keys -X begin-selection || fail "begin-selection failed"
|
||||
i=0
|
||||
while [ $i -lt 6 ]; do
|
||||
$TMUX send-keys -X cursor-down || fail "cursor-down failed"
|
||||
sleep 0.15
|
||||
i=$((i + 1))
|
||||
done
|
||||
sleep 0.5
|
||||
|
||||
# No DCS sequence should have been sent - the image's row was never
|
||||
# disturbed by any of this. This is expected to fail before the fix - see
|
||||
# the header comment.
|
||||
dcs=$(grep -ac "$(printf '\033P')" $TMP)
|
||||
[ "$dcs" -eq 0 ] ||
|
||||
fail "image was retransmitted ($dcs times) while just moving the selection cursor"
|
||||
|
||||
exit 0
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Regression test for the imagescroll terminal-feature (tty-features.c),
|
||||
# SIXEL side - see image-kitty-region-scroll.sh for the Kitty side of the
|
||||
# same feature: scrolling a pane that has a SIXEL image in it must not
|
||||
# retransmit the image when the client's terminal has imagescroll - tmux
|
||||
# trusts the terminal to have moved the image along with the rest of the
|
||||
# scrolling region. Without imagescroll (the default for any terminal not
|
||||
# individually confirmed and added to tty-features.c's table), 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: imagescroll granted - expect no DCS. ---
|
||||
$TMUX set -as terminal-features ',*:imagescroll' || fail "grant imagescroll 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 imagescroll granted"
|
||||
|
||||
# --- Phase 2: imagescroll not granted (the default) - 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 -as terminal-features ',*:imagescroll@' || fail "revoke imagescroll 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 without imagescroll - expected the default always-redraw behaviour"
|
||||
|
||||
exit 0
|
||||
Executable
+362
@@ -0,0 +1,362 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Grid-resident image layers: Kitty input, history, copy and overwrite.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
TMUX="$TEST_TMUX -Limage$$ -f/dev/null"
|
||||
TMUX2="$TEST_TMUX -Limage-client$$ -f/dev/null"
|
||||
$TMUX kill-server 2>/dev/null
|
||||
$TMUX2 kill-server 2>/dev/null
|
||||
|
||||
TMP=$(mktemp)
|
||||
FIXTURE=$(pwd)/monkey-2.sixel.txt
|
||||
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15
|
||||
|
||||
$TMUX new-session -d -x 20 -y 8 "
|
||||
printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,m=1;/wAA/wD/\033\\'
|
||||
printf '\033_Gm=0;AP8AAP///////w==\033\\'
|
||||
i=0
|
||||
while [ \$i -lt 10 ]; do printf '\nline-%d' \$i; i=\$((i + 1)); done
|
||||
sleep 10"
|
||||
|
||||
sleep 1
|
||||
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
|
||||
|
||||
# Images scroll as grid cells, while capture output contains ordinary spaces.
|
||||
[ "$($TMUX display-message -p '#{history_size}')" -gt 0 ] || exit 1
|
||||
$TMUX capture-pane -pS- >$TMP || exit 1
|
||||
grep '_G' $TMP >/dev/null && exit 1
|
||||
|
||||
# Copy mode duplicates the backing grid, including image layers.
|
||||
$TMUX copy-mode || exit 1
|
||||
$TMUX send-keys -X history-top || exit 1
|
||||
$TMUX capture-pane -p >$TMP || exit 1
|
||||
$TMUX send-keys -X cancel || exit 1
|
||||
|
||||
# Ordinary text updates the image underlay through the normal grid write path.
|
||||
# Deleting the image reveals the newly written text.
|
||||
$TMUX new-window -d "
|
||||
printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,i=8;/wAA/wD/AP8AAP///////w==\033\\'
|
||||
printf '\033[HXY'
|
||||
printf '\033_Ga=d,d=i,q=2,i=8\033\\'
|
||||
sleep 10"
|
||||
sleep 1
|
||||
[ "$($TMUX capture-pane -pt:1 -S0 -E0)" = "XY" ] || exit 1
|
||||
|
||||
# The protocol default is transmit-only; a later placement uses the retained
|
||||
# immutable image and advances by the same canonical one-cell footprint.
|
||||
$TMUX new-window -d "
|
||||
printf '\033_Gq=2,f=32,s=1,v=1,c=1,r=1,i=9;/wAA/w==\033\\'
|
||||
printf '\033_Ga=p,q=2,i=9\033\\'
|
||||
printf '\033_Ga=d,d=I,q=2,i=9\033\\'
|
||||
sleep 10"
|
||||
sleep 1
|
||||
[ "$($TMUX display-message -pt:2 '#{cursor_y}')" = 1 ] || exit 1
|
||||
|
||||
# PNG Kitty input uses the shared image decoder and canonical cell sizing.
|
||||
$TMUX new-window -d "
|
||||
printf '\033_Ga=T,q=2,f=100;iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURf8AAP///0EdNBEAAAABYktHRAH/Ai3eAAAAB3RJTUUH6ggCDAECH324BwAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=\033\\'
|
||||
sleep 10"
|
||||
sleep 1
|
||||
[ "$($TMUX display-message -pt:3 '#{cursor_y}')" = 1 ] || exit 1
|
||||
|
||||
# SIXEL input reaches the same grid layer and copy-mode paths.
|
||||
$TMUX new-window -d "cat '$FIXTURE'; sleep 10"
|
||||
sleep 1
|
||||
[ "$($TMUX display-message -pt:4 '#{cursor_y}')" -gt 0 ] || exit 1
|
||||
$TMUX copy-mode -t:4 || exit 1
|
||||
$TMUX send-keys -t:4 -X history-top || exit 1
|
||||
$TMUX capture-pane -pt:4 >$TMP || exit 1
|
||||
|
||||
# A 26-pixel raster occupies two default 16-pixel cells, but the final cell
|
||||
# remains only partially filled instead of stretching the raster to 32 pixels.
|
||||
# A client without an image feature exposes this in the sampled text backend.
|
||||
$TMUX kill-server 2>/dev/null
|
||||
$TMUX2 new-session -d -x 10 -y 4 "
|
||||
printf '\033Pq\"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
|
||||
sleep 10" || exit 1
|
||||
$TMUX2 set -g status off || exit 1
|
||||
$TMUX2 set -as terminal-features ',*:sixel@' || exit 1
|
||||
$TMUX new-session -d -x 10 -y 4 || exit 1
|
||||
$TMUX set -g status off || exit 1
|
||||
$TMUX pipe-pane -O "cat >$TMP" || exit 1
|
||||
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
|
||||
$TMUX send-keys Enter || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
|
||||
grep -q '^#=' $TMP || exit 1
|
||||
|
||||
# Re-emitting the image preserves the 26-pixel raster rather than expanding
|
||||
# it to the two complete 16-pixel grid cells occupied by the image.
|
||||
# terminal-features is only detected when a client attaches, so switch to a
|
||||
# SIXEL-capable client by detaching and reattaching rather than toggling the
|
||||
# option on the client already attached above.
|
||||
$TMUX2 set -u terminal-features || exit 1
|
||||
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
|
||||
$TMUX2 detach-client || exit 1
|
||||
sleep 1
|
||||
$TMUX pipe-pane || exit 1
|
||||
$TMUX pipe-pane -O "cat >$TMP" || exit 1
|
||||
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
|
||||
$TMUX send-keys Enter || exit 1
|
||||
sleep 1
|
||||
grep -a '"1;1;26;26' $TMP >/dev/null || exit 1
|
||||
|
||||
# A selection redraw uses the single-cell path. It must leave ASCII image
|
||||
# cells visible (graphical clients skip these cells to avoid erasing pixels).
|
||||
# Switch back to a fallback client the same way.
|
||||
$TMUX2 set -u terminal-features || exit 1
|
||||
$TMUX2 set -as terminal-features ',*:sixel@' || exit 1
|
||||
$TMUX2 detach-client || exit 1
|
||||
sleep 1
|
||||
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
|
||||
$TMUX send-keys Enter || exit 1
|
||||
sleep 1
|
||||
$TMUX2 copy-mode || exit 1
|
||||
$TMUX2 send-keys -X history-top || exit 1
|
||||
$TMUX2 send-keys -X start-of-line || exit 1
|
||||
$TMUX2 send-keys -X begin-selection || exit 1
|
||||
$TMUX2 send-keys -X cursor-right || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
|
||||
grep -q '^#=' $TMP || exit 1
|
||||
|
||||
# A retained Kitty image may be placed repeatedly using source rectangles.
|
||||
# Crop the green and white right column from a red/green/blue/white image.
|
||||
$TMUX kill-server 2>/dev/null
|
||||
$TMUX2 kill-server 2>/dev/null
|
||||
$TMUX2 new-session -d -x 10 -y 4 "
|
||||
printf '\033_Ga=t,q=2,f=32,s=2,v=2,i=10;/wAA/wD/AP8AAP///////w==\033\\'
|
||||
printf '\033_Ga=p,q=2,i=10,x=1,y=0,w=1,h=2,c=1,r=2\033\\'
|
||||
sleep 10" || exit 1
|
||||
$TMUX2 set -g status off || exit 1
|
||||
$TMUX new-session -d -x 10 -y 4 || exit 1
|
||||
$TMUX set -g status off || exit 1
|
||||
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
|
||||
$TMUX send-keys Enter || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP)" = "*" ] || exit 1
|
||||
[ "$(sed -n 2p $TMP)" = "@" ] || exit 1
|
||||
|
||||
# C=1 places the image without moving the cursor or scrolling. The four-row
|
||||
# image is clipped at the bottom of this four-row pane, leaving its white last
|
||||
# row off screen.
|
||||
$TMUX2 new-window -d "
|
||||
printf '\033[2;3H'
|
||||
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\'
|
||||
sleep 10" || exit 1
|
||||
$TMUX2 select-window -t:1 || exit 1
|
||||
sleep 1
|
||||
[ "$($TMUX2 display-message -p '#{cursor_x},#{cursor_y}')" = "2,1" ] || exit 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ "$(sed -n 3p $TMP)" = " -" ] || exit 1
|
||||
[ "$(sed -n 4p $TMP)" = " *" ] || exit 1
|
||||
|
||||
# With normal cursor movement, scrolling is calculated from the full image
|
||||
# height. Rows which scrolled above the pane are then cropped from the top, so
|
||||
# the bottom three source rows remain visible and the cursor is on the last row.
|
||||
$TMUX2 new-window -d "
|
||||
printf '\033[3;1H'
|
||||
printf '\033_Ga=T,q=2,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\'
|
||||
sleep 10" || exit 1
|
||||
$TMUX2 select-window -t:2 || exit 1
|
||||
sleep 1
|
||||
[ "$($TMUX2 display-message -p '#{cursor_y}')" = 3 ] || exit 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP)" = "-" ] || exit 1
|
||||
[ "$(sed -n 2p $TMP)" = "*" ] || exit 1
|
||||
[ "$(sed -n 3p $TMP)" = "@" ] || exit 1
|
||||
|
||||
# Image rows remain cell-aligned when a narrower terminal causes text
|
||||
# reflow. The ten-column rows are clipped to five columns, not split into four
|
||||
# wrapped rows.
|
||||
$TMUX2 new-window -d "
|
||||
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=2,c=10,r=2;/wAA//////8=\033\\'
|
||||
sleep 10" || exit 1
|
||||
$TMUX2 select-window -t:3 || exit 1
|
||||
sleep 1
|
||||
$TMUX resize-window -x 5 -y 4 || exit 1
|
||||
sleep 1
|
||||
[ "$($TMUX2 display-message -p '#{window_width}x#{window_height}')" = "5x4" ] || exit 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP)" = "....." ] || exit 1
|
||||
[ "$(sed -n 2p $TMP)" = "@@@@@" ] || exit 1
|
||||
[ -z "$(sed -n 3p $TMP)" ] || exit 1
|
||||
|
||||
# Kitty virtual placements use U+10EEEE placeholder cells. Keep their rows at
|
||||
# fixed coordinates when narrowing the terminal, clipping instead of reflowing
|
||||
# the second half onto the following row.
|
||||
$TMUX resize-window -x 10 -y 4 || exit 1
|
||||
sleep 1
|
||||
$TMUX2 new-window -d "
|
||||
printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205'
|
||||
printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205'
|
||||
sleep 10" || exit 1
|
||||
$TMUX2 select-window -t:4 || exit 1
|
||||
sleep 1
|
||||
$TMUX resize-window -x 5 -y 4 || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ -n "$(sed -n 1p $TMP)" ] || exit 1
|
||||
[ -z "$(sed -n 2p $TMP)" ] || exit 1
|
||||
$TMUX resize-window -x 10 -y 4 || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP | wc -c)" = 61 ] || exit 1
|
||||
[ -z "$(sed -n 2p $TMP)" ] || exit 1
|
||||
|
||||
# A placement ID supplied with transmit-and-place is reused by a later place.
|
||||
# This is the sequence used by chawan: the newline moves the cursor down before
|
||||
# the second command, so the folder moves down one row without leaving a copy.
|
||||
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
|
||||
printf '\033_GC=1,s=26,v=26,p=1,q=2,i=1,a=T,f=100,m=0;iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAACmUlEQVR4XmOgF2CEWVRfX8/w//9/BkZGRgy7YeKNjY1kuwtsam1tLQMTE5MG0MBsoEWW6KYBxf8DxScD6UVNTU1kWcZYU1MD0qjLzMx8EmgYJy5TgJb8/ffvn+2vX7+Od3Z2kmwZC9AnIE0FIEuAhl0H4ulA9k9kk4Bi4UB1TkA8H2iRYXl5+Xd2dna8loGCG2Q2LLgZQXED1PEIaLjs379/g4Ds9VDLwQYBxRiAPpFmZWW9DFQjCDRgLxDfJeQloJpdQHPWA9X9A1kGs+gZ0BBJoKQtUOIIeqSDgheoKR6IF5ASZkAH3gKaCzLzFQsxGltaWhjq6uoWguIJ6hi82oCG8wIV+AEdpga0LBfIrmUh1oWg1AYM5iVA9UuI1JMGVDcTiCNIsghkOChIQ0NDwfasXr0ap33QeL8GUgD0HTeIZmIgAUAtYQZqYYZZSKx2oi2CWaKpqbkIhEm1jOg40tLSAjleEBgUUSAGkJ8PpN8Q6yOCFoF8AvQBTvOAqZHh+vXrDPjijGAcIcXJUqBPQOXda5iNQP5rkBiQv5SYYMTrIxUVFZC5IkDCCk8QWQHVgdS8xBeMeBNDe3s7A7CwfQl0uSmwKDIBYhdQGQbCIDZIDCQHUgNSS7ZFII2gUgEU6UADzwJz+kUgDa6zoOyzIDmoGgaKLAJpBheKkArxC7BIeQDCQPEvIEuJrQyJzkdQA38ADbcHYaBFP0ipcVGqCWDYBwPxOkYs1TkDCeA/pEkQBDRnLZD9GKhVjgUkCGTsBgomAekWIJYG4p8MlAFQrZgJMgJo/m4QzQJ1fT9QIBLI1gTiSRRaAm/gAM38DjSvH2QeuHECLW1xNk7IsRhoyXGgJVOBem+AExPMEHzNLTItYiAlVTJQCwAAJxkMkgbHo2UAAAAASUVORK5CYII=\033\\'
|
||||
printf '\n'
|
||||
sleep 1
|
||||
printf '\033_GC=1,s=26,v=26,p=1,q=2,i=1,a=p;\033\\'
|
||||
printf '\n'
|
||||
sleep 10") || exit 1
|
||||
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ -z "$(sed -n 1p $TMP)" ] || exit 1
|
||||
[ -n "$(sed -n 2p $TMP)" ] || exit 1
|
||||
|
||||
# A nonzero Kitty placement ID identifies one placement of an image. Reusing
|
||||
# the same image and placement IDs moves it rather than leaving the old cells.
|
||||
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
|
||||
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=11;/////w==\033\\'
|
||||
printf '\033[2;2H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\'
|
||||
printf '\033[4;6H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\'
|
||||
sleep 10") || exit 1
|
||||
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ -z "$(sed -n 2p $TMP)" ] || exit 1
|
||||
[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1
|
||||
|
||||
# Deleting one placement ID leaves other placements of the image intact.
|
||||
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
|
||||
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=12;/////w==\033\\'
|
||||
printf '\033[2;2H\033_Ga=p,q=2,C=1,i=12,p=7,c=2,r=1\033\\'
|
||||
printf '\033[4;6H\033_Ga=p,q=2,C=1,i=12,p=8,c=2,r=1\033\\'
|
||||
printf '\033_Ga=d,d=i,q=2,i=12,p=7\033\\'
|
||||
sleep 10") || exit 1
|
||||
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ -z "$(sed -n 2p $TMP)" ] || exit 1
|
||||
[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1
|
||||
|
||||
# Image layers retain the cells beneath them. Deleting a transparent image
|
||||
# must reveal the original text rather than replacing it with spaces.
|
||||
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
|
||||
printf 'XY'
|
||||
printf '\033[H'
|
||||
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=1,c=2,r=1,i=13,p=7;////AA==\033\\'
|
||||
printf '\033_Ga=d,d=i,q=2,i=13,p=7\033\\'
|
||||
sleep 10") || exit 1
|
||||
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
|
||||
sleep 1
|
||||
[ "$($TMUX2 capture-pane -pS0 -E0)" = "XY" ] || exit 1
|
||||
|
||||
# Text written over a Kitty placement updates its underlay without deleting
|
||||
# the image. Deleting the placement afterwards reveals the updated text.
|
||||
TEXT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
|
||||
printf 'test\r'
|
||||
printf '\033_Ga=T,q=2,C=1,f=32,s=2,v=2,c=2,r=2,i=14,p=7;/wAA/wD/AP8AAP///////w==\033\\'
|
||||
printf '\r'
|
||||
printf 'test\n'
|
||||
printf 'test\n'
|
||||
$TEST_TMUX wait-for image-text-delete-$$
|
||||
printf '\033_Ga=d,d=i,q=2,i=14,p=7\033\\'
|
||||
sleep 10") || exit 1
|
||||
$TMUX2 select-window -t"$TEXT_WINDOW" || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP)" = ".*st" ] || exit 1
|
||||
[ "$(sed -n 2p $TMP)" = " @st" ] || exit 1
|
||||
$TMUX2 wait-for -S image-text-delete-$$ || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP)" = "test" ] || exit 1
|
||||
[ "$(sed -n 2p $TMP)" = "test" ] || exit 1
|
||||
|
||||
# Kitty z-indexes and SIXEL's temporal plane are properties of the input, not
|
||||
# of the outer terminal. Exercise the logical scene through the fallback
|
||||
# backend: higher nonnegative z-indexes cover lower ones and text, ordinary
|
||||
# negative z-indexes cover backgrounds but not text, and very negative ones
|
||||
# remain below the background.
|
||||
Z_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
|
||||
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=21;/wAA/w==\033\\'
|
||||
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=22;/////w==\033\\'
|
||||
printf 'X\r'
|
||||
printf '\033_Ga=p,q=2,C=1,i=21,p=1,z=2,c=1,r=1\033\\'
|
||||
printf '\033_Ga=p,q=2,C=1,i=22,p=2,z=1,c=1,r=1\033\\'
|
||||
printf '\033[2;1H\033[41m \033[0m\r'
|
||||
printf '\033_Ga=p,q=2,C=1,i=21,p=3,z=-1,c=1,r=1\033\\'
|
||||
printf '\033[3;1HY\r'
|
||||
printf '\033_Ga=p,q=2,C=1,i=21,p=4,z=-1,c=1,r=1\033\\'
|
||||
printf '\033[4;1H\033[41m \033[0m\r'
|
||||
printf '\033_Ga=p,q=2,C=1,i=21,p=5,z=-1073741825,c=1,r=1\033\\'
|
||||
$TEST_TMUX wait-for image-z-top-$$
|
||||
printf '\033_Ga=d,d=z,q=2,z=2\033\\'
|
||||
$TEST_TMUX wait-for image-z-lower-$$
|
||||
printf '\033_Ga=d,d=z,q=2,z=1\033\\'
|
||||
sleep 10") || exit 1
|
||||
$TMUX2 select-window -t"$Z_WINDOW" || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP)" = "." ] || exit 1
|
||||
[ "$(sed -n 2p $TMP)" = "." ] || exit 1
|
||||
[ "$(sed -n 3p $TMP)" = "Y" ] || exit 1
|
||||
[ -z "$(sed -n 4p $TMP)" ] || exit 1
|
||||
$TMUX2 wait-for -S image-z-top-$$ || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP)" = "@" ] || exit 1
|
||||
$TMUX2 wait-for -S image-z-lower-$$ || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
|
||||
[ "$(sed -n 1p $TMP)" = "X" ] || exit 1
|
||||
|
||||
# A weighted median at the maximum channel level must still leave colours on
|
||||
# both sides of the split. This skewed black, grey and white image used to stop
|
||||
# palette generation after one colour instead of producing three.
|
||||
$TMUX kill-server 2>/dev/null
|
||||
$TMUX2 kill-server 2>/dev/null
|
||||
$TMUX2 new-session -d -x 20 -y 4 "
|
||||
printf '\033_Ga=T,q=2,C=1,f=24,s=100,v=1,c=10,r=1;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\033\\'
|
||||
sleep 10" || exit 1
|
||||
$TMUX2 set -g status off || exit 1
|
||||
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
|
||||
$TMUX new-session -d -x 20 -y 4 || exit 1
|
||||
$TMUX set -g status off || exit 1
|
||||
$TMUX pipe-pane -O "cat >$TMP" || exit 1
|
||||
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
|
||||
$TMUX send-keys Enter || exit 1
|
||||
sleep 1
|
||||
grep -a '#2;2;' $TMP >/dev/null || exit 1
|
||||
|
||||
# A pane redraw on a SIXEL terminal must not clear the status line, which is
|
||||
# outside the window scene and may not be redrawn after the pane scrolls.
|
||||
$TMUX kill-server 2>/dev/null
|
||||
$TMUX2 kill-server 2>/dev/null
|
||||
$TMUX2 new-session -d -x 20 -y 8 || exit 1
|
||||
$TMUX2 set -g 'status-format[0]' 'STATUS' || exit 1
|
||||
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
|
||||
$TMUX new-session -d -x 20 -y 8 || exit 1
|
||||
$TMUX set -g status off || exit 1
|
||||
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
|
||||
$TMUX send-keys Enter || exit 1
|
||||
sleep 1
|
||||
$TMUX2 send-keys -l 'seq 30' || exit 1
|
||||
$TMUX2 send-keys Enter || exit 1
|
||||
sleep 1
|
||||
$TMUX capture-pane -p >$TMP || exit 1
|
||||
[ "$(tail -n 1 $TMP)" = "STATUS" ] || exit 1
|
||||
|
||||
exit 0
|
||||
@@ -60,7 +60,11 @@ query_timeout()
|
||||
|
||||
query "dsr-ok" '^[[0n' '\033[5n' 4 ''
|
||||
query "dsr-cursor" '^[[1;1R' '\033[6n' 6 ''
|
||||
query "da-primary" '^[[?1;2c' '\033[c' 7 ''
|
||||
if [ "$($TMUX display-message -p '#{image_support}')" = 1 ]; then
|
||||
query "da-primary" '^[[?1;2;4c' '\033[c' 9 ''
|
||||
else
|
||||
query "da-primary" '^[[?1;2c' '\033[c' 7 ''
|
||||
fi
|
||||
query "da-secondary" '^[[>84;0;0c' '\033[>c' 10 ''
|
||||
query "decrqm-irm-reset" '^[[4;2$y' '\033[4$p' 7 ''
|
||||
query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 7 '\033[4h'
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Tests that the scroll position of a mode tree list (mode-tree.c) is kept
|
||||
# when the list is rebuilt, using choose-buffer. Buffer mode rebuilds its list
|
||||
# every time the status line is redrawn, which used to move the view so the
|
||||
# selected line was on the bottom row (or the list jumped back to the top)
|
||||
# even if the selected line was already visible. Also checks that when the
|
||||
# list shrinks the view does not leave empty lines at the bottom.
|
||||
#
|
||||
# The list is drawn on a mode screen which capture-pane does not show, so - as
|
||||
# in choose-buffer.sh - a second server provides a client: an inner "tmux
|
||||
# attach" runs inside a pane of the second server, and that pane is captured
|
||||
# to read what the inner client rendered.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
TMP=$(mktemp -d) || exit 1
|
||||
TMUX_TMPDIR="$TMP"
|
||||
export TMUX_TMPDIR
|
||||
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
|
||||
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
|
||||
|
||||
cleanup()
|
||||
{
|
||||
$TMUX kill-server 2>/dev/null
|
||||
$TMUX2 kill-server 2>/dev/null
|
||||
rm -rf "$TMP"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# capture the screen rendered by the inner client
|
||||
capture()
|
||||
{
|
||||
$TMUX2 capture-pane -p -t out:0 2>/dev/null
|
||||
}
|
||||
|
||||
# wait_for $marker
|
||||
#
|
||||
# Wait (up to ~10s) until the rendered screen contains $marker. The matching
|
||||
# capture is left in CAPTURED.
|
||||
wait_for()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
CAPTURED=$(capture)
|
||||
if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.2
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "timed out waiting for '$1'"
|
||||
}
|
||||
|
||||
# wait_gone $marker
|
||||
#
|
||||
# Wait (up to ~10s) until the rendered screen no longer contains $marker. The
|
||||
# matching capture is left in CAPTURED.
|
||||
wait_gone()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
CAPTURED=$(capture)
|
||||
if ! printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.2
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "timed out waiting for '$1' to disappear"
|
||||
}
|
||||
|
||||
# wait_clients $n
|
||||
#
|
||||
# Wait (up to ~10s) until the test server has exactly $n clients attached.
|
||||
wait_clients()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 10 ]; do
|
||||
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
|
||||
[ "$c" -eq "$1" ] && return 0
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# first_line
|
||||
#
|
||||
# Print the name of the first buffer shown in CAPTURED.
|
||||
first_line()
|
||||
{
|
||||
printf '%s\n' "$CAPTURED" | grep -F ': S1' | head -1 | \
|
||||
sed 's/.*\(buf[0-9a-z]*\): S1.*/\1/'
|
||||
}
|
||||
|
||||
$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1
|
||||
|
||||
$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1
|
||||
wait_clients 1 || fail "no client attached to test server"
|
||||
|
||||
# Forty buffers, more than fit on the screen.
|
||||
n=0
|
||||
while [ "$n" -lt 40 ]; do
|
||||
$TMUX set-buffer -b "$(printf 'buf%02d' "$n")" "buffer $n" || exit 1
|
||||
n=$((n + 1))
|
||||
done
|
||||
|
||||
# Without a preview the list has the whole pane (23 lines with the status
|
||||
# line), so buf00 to buf22 are visible.
|
||||
$TMUX choose-buffer -t aaa:0 -N -O name -F 'S1' || exit 1
|
||||
wait_for 'buf00: S1'
|
||||
|
||||
# --- scrolling down moves the view -------------------------------------------
|
||||
#
|
||||
# Selecting buf30 scrolls so it is on the bottom row: buf08 is at the top.
|
||||
$TMUX send-keys -t aaa:0 -N 30 Down || exit 1
|
||||
wait_for 'buf30: S1'
|
||||
top=$(first_line)
|
||||
[ "$top" = "buf08" ] || fail "top line after scrolling is $top, not buf08"
|
||||
|
||||
# Moving up to buf20 stays inside the visible lines so the view does not move.
|
||||
$TMUX send-keys -t aaa:0 -N 10 Up || exit 1
|
||||
|
||||
# --- a rebuild keeps the view where it is ------------------------------------
|
||||
#
|
||||
# Add a buffer that sorts into the visible part of the list and redraw the
|
||||
# status line so the list is rebuilt. Once buf15a appears the rebuild has
|
||||
# happened; buf08 must still be the top line. It used to jump back to buf00.
|
||||
$TMUX set-buffer -b buf15a "new buffer" || exit 1
|
||||
$TMUX refresh-client -S || exit 1
|
||||
wait_for 'buf15a: S1'
|
||||
top=$(first_line)
|
||||
[ "$top" = "buf08" ] || fail "top line after rebuild is $top, not buf08"
|
||||
|
||||
# --- a shrinking list does not leave empty lines -----------------------------
|
||||
#
|
||||
# Go to the end of the list (buf39 is on the bottom row) and delete all but
|
||||
# the last ten buffers. They now all fit, so the view goes to the top.
|
||||
$TMUX send-keys -t aaa:0 End || exit 1
|
||||
wait_for 'buf39: S1'
|
||||
for b in $($TMUX list-buffers -F '#{buffer_name}'); do
|
||||
case "$b" in
|
||||
buf3?)
|
||||
;;
|
||||
*)
|
||||
$TMUX delete-buffer -b "$b" || exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
$TMUX refresh-client -S || exit 1
|
||||
wait_gone 'buf29: S1'
|
||||
top=$(first_line)
|
||||
[ "$top" = "buf30" ] || fail "top line after shrinking is $top, not buf30"
|
||||
[ "$(printf '%s\n' "$CAPTURED" | grep -F -c ': S1')" -eq 10 ] || \
|
||||
fail "expected 10 buffers after shrinking"
|
||||
|
||||
$TMUX send-keys -t aaa:0 q
|
||||
|
||||
exit 0
|
||||
File diff suppressed because one or more lines are too long
@@ -1,126 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Damage at a popup edge must redraw complete grid characters. Drawing only a
|
||||
# wide character's base or padding cell leaves a two-cell hole behind.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
LC_ALL=C.UTF-8
|
||||
export PATH TERM LC_ALL
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
INNER="$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lpopup-wide-outer-$$ -f/dev/null"
|
||||
EMITTER=$DIR/emitter.pl
|
||||
BASE=$DIR/base
|
||||
CAPTURE=$DIR/capture
|
||||
POPUP_PID=
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup()
|
||||
{
|
||||
[ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
|
||||
wait_for_client()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
|
||||
[ -n "$CLIENT" ] && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "inner client did not attach"
|
||||
}
|
||||
|
||||
wait_outer_has()
|
||||
{
|
||||
marker=$1
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
|
||||
grep -q "$marker" "$CAPTURE" && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "outer client did not show $marker"
|
||||
}
|
||||
|
||||
wait_old_rows_restored()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
|
||||
sed -n '3,5p' "$BASE" >"$DIR/want"
|
||||
sed -n '3,5p' "$CAPTURE" >"$DIR/got"
|
||||
cmp -s "$DIR/want" "$DIR/got" && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "wide characters under the old popup edge were not restored"
|
||||
}
|
||||
|
||||
mouse()
|
||||
{
|
||||
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
|
||||
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
|
||||
sleep 0.1
|
||||
}
|
||||
|
||||
cat >"$EMITTER" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
binmode STDOUT, ':encoding(UTF-8)';
|
||||
$| = 1;
|
||||
for my $row (1 .. 10) {
|
||||
print "\e[$row;1H", chr(0x754c) x 20;
|
||||
}
|
||||
sleep 100;
|
||||
PERL
|
||||
|
||||
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
$INNER set-option -g mouse on || exit 1
|
||||
|
||||
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
|
||||
$OUTER set-option -g status off || exit 1
|
||||
$OUTER set-option -g window-size manual || exit 1
|
||||
$OUTER set-option -g default-terminal screen-256color || exit 1
|
||||
$OUTER respawn-pane -k -t outer:0.0 \
|
||||
"$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null attach-session -t inner" ||
|
||||
exit 1
|
||||
|
||||
wait_for_client
|
||||
wait_outer_has '界界界'
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
|
||||
|
||||
$INNER display-popup -t "$CLIENT" -x 5 -y 5 -w 10 -h 3 -E \
|
||||
"sh -c 'printf POPUP; exec sleep 100'" &
|
||||
POPUP_PID=$!
|
||||
wait_outer_has POPUP
|
||||
|
||||
# The first motion starts the drag; the second moves the popup away from its
|
||||
# old rectangle. Its odd x coordinate bisects the underlying double-width
|
||||
# cells at both edges.
|
||||
mouse 0 10 3 M
|
||||
mouse 32 11 3 M
|
||||
mouse 32 28 7 M
|
||||
mouse 0 28 7 m
|
||||
wait_old_rows_restored
|
||||
|
||||
exit 0
|
||||
@@ -131,14 +131,6 @@ printf '%s\n' "$captured" | grep -Fq 'show-buffer' ||
|
||||
printf '%s\n' "$captured" | grep -Fq 'show-environment' ||
|
||||
fail "ambiguous completion list was incomplete"
|
||||
$OUTER send-keys Escape || exit 1
|
||||
# Wait until Escape has closed the prompt before sending another Meta key.
|
||||
# Otherwise the two can be parsed together as a single escape sequence.
|
||||
i=0
|
||||
while capture | grep -Fq '(word)'; do
|
||||
[ "$i" -lt 50 ] || fail "completion prompt did not close"
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
# Add entries to both history rings through real prompts.
|
||||
bind_prompt 'history-command'
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Many wrapped rows crossing a panned viewport produce disjoint damage.
|
||||
# Every row must survive the rectangle-count limit and subsequent merging.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
LC_ALL=C.UTF-8
|
||||
export PATH TERM LC_ALL
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
INNER="$TEST_TMUX -Laccumulate-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Laccumulate-outer-$$ -f/dev/null"
|
||||
cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
wait_marker()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$DIR/capture" || exit 1
|
||||
grep -q "$1" "$DIR/capture" && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "client did not receive $1"
|
||||
}
|
||||
cat >"$DIR/emitter.pl" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
$| = 1;
|
||||
for my $row (1 .. 80) {
|
||||
printf "\e[%d;1HROW%02d-%s", $row, $row, 'abcdefghij' x 6;
|
||||
}
|
||||
print "\e[1;1H";
|
||||
while (!-e $ENV{TRIGGER}) {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
my $output = '';
|
||||
# Each wrapped second row needs a separate damage rectangle. Keep the
|
||||
# batch below the PTY read size so all 18 arrive in the same input pass.
|
||||
for my $region (0 .. 17) {
|
||||
my $top = 1 + $region * 4;
|
||||
$output .= "\e[$top;1H" . ('A' x 80) . ('B' x 24) .
|
||||
sprintf('DAMAGE%02d', $region) . ('B' x 48);
|
||||
}
|
||||
$output .= "\e[80;21HDONE";
|
||||
syswrite STDOUT, $output;
|
||||
while (!-e "$ENV{TRIGGER}-merge") {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
# Returning from the alternate screen requests a full-pane rectangle. A
|
||||
# later wrapped-row rectangle overlaps it and must not shrink that damage.
|
||||
$output = "\e[?1049h\e[40;21HALT-VISIBLE\e[?1049l";
|
||||
$output .= "\e[1;1H" . ('C' x 80) . ('D' x 24) . 'MERGE00' . ('D' x 49);
|
||||
$output .= "\e[80;21HMERGED";
|
||||
syswrite STDOUT, $output;
|
||||
sleep 100;
|
||||
PERL
|
||||
$INNER new-session -d -s inner -x 80 -y 80 \
|
||||
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
|
||||
$INNER set -g status off || exit 1
|
||||
$INNER set -g window-size manual || exit 1
|
||||
$INNER set -g automatic-rename off || exit 1
|
||||
$INNER set -g status-interval 0 || exit 1
|
||||
$INNER set -as terminal-features ',screen:sync' || exit 1
|
||||
$INNER set -g pane-border-lines simple || exit 1
|
||||
$OUTER new-session -d -s outer -x 40 -y 80 'sleep 100' || exit 1
|
||||
$OUTER set -g status off || exit 1
|
||||
$OUTER set -g window-size manual || exit 1
|
||||
$OUTER set -g default-terminal screen || exit 1
|
||||
$OUTER respawn-pane -k -t outer:0.0 "$INNER attach -t inner" || exit 1
|
||||
wait_marker ROW80
|
||||
CLIENT=$($OUTER display -p -t outer:0.0 '#{pane_tty}') || exit 1
|
||||
$INNER refresh-client -t "$CLIENT" -R 20 || exit 1
|
||||
# Keep the emitter inactive so its writes use a synchronized frame. Otherwise
|
||||
# its own queued output can force a full redraw and hide lost rectangles.
|
||||
$INNER new-pane -x 6 -y 3 -X 65 -Y 74 'sleep 100' || exit 1
|
||||
sleep 0.2
|
||||
: >"$DIR/trigger"
|
||||
wait_marker DONE
|
||||
sleep 0.2
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$DIR/before" || exit 1
|
||||
region=0
|
||||
while [ "$region" -lt 18 ]; do
|
||||
marker=$(printf 'DAMAGE%02d' "$region")
|
||||
sed -n "$((2 + region * 4))p" "$DIR/before" | grep -q "$marker" ||
|
||||
fail "missing damage for region $region"
|
||||
region=$((region + 1))
|
||||
done
|
||||
$INNER refresh-client -t "$CLIENT" || exit 1
|
||||
sleep 0.2
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$DIR/after" || exit 1
|
||||
diff -u "$DIR/before" "$DIR/after" || fail "accumulated redraw differs from full redraw"
|
||||
: >"$DIR/trigger-merge"
|
||||
wait_marker MERGED
|
||||
sleep 0.2
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$DIR/before" || exit 1
|
||||
sed -n '2p' "$DIR/before" | grep -q MERGE00 || fail "merged damage lost the wrapped row"
|
||||
$INNER refresh-client -t "$CLIENT" || exit 1
|
||||
sleep 0.2
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$DIR/after" || exit 1
|
||||
diff -u "$DIR/before" "$DIR/after" || fail "merged redraw differs from full redraw"
|
||||
exit 0
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# One terminal stops reading while another consumes floating-pane damage.
|
||||
# The slow client must catch up after the window's shared damage is cleared.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
LC_ALL=C.UTF-8
|
||||
export PATH TERM LC_ALL
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
INNER="$TEST_TMUX -Lblocked-inner-$$ -f/dev/null"
|
||||
FAST="$TEST_TMUX -Lblocked-fast-$$ -f/dev/null"
|
||||
SLOW="$TEST_TMUX -Lblocked-slow-$$ -f/dev/null"
|
||||
STOPPED=
|
||||
cleanup()
|
||||
{
|
||||
[ -n "$STOPPED" ] && kill -CONT "$STOPPED" 2>/dev/null
|
||||
$FAST kill-server 2>/dev/null
|
||||
$SLOW kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
wait_marker()
|
||||
{
|
||||
terminal=$1
|
||||
marker=$2
|
||||
i=0
|
||||
while [ "$i" -lt 100 ]; do
|
||||
$terminal capture-pane -p >"$DIR/capture" || exit 1
|
||||
grep -q "$marker" "$DIR/capture" && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "client did not receive $marker"
|
||||
}
|
||||
mouse()
|
||||
{
|
||||
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
|
||||
$FAST send-keys -l "$sequence" || exit 1
|
||||
sleep 0.2
|
||||
}
|
||||
cat >"$DIR/emitter.pl" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
$| = 1;
|
||||
print 'READY';
|
||||
while (!-e "$ENV{TRIGGER}-flood") {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
my $frame = 0;
|
||||
while (!-e "$ENV{TRIGGER}-stop") {
|
||||
my $output = '';
|
||||
for my $row (1 .. 20) {
|
||||
$output .= "\e[$row;1H" . join('', map { chr(33 + ($_ + $frame) % 80) } 0 .. 77);
|
||||
}
|
||||
print $output;
|
||||
$frame++;
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
for my $row (1 .. 20) {
|
||||
printf "\e[%d;1HFINAL%02d-%s", $row, $row, '0123456789' x 7;
|
||||
}
|
||||
print "\e[20;1HDONE";
|
||||
sleep 100;
|
||||
PERL
|
||||
$INNER new-session -d -s inner -x 160 -y 80 \
|
||||
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
|
||||
$INNER set -g status off || exit 1
|
||||
$INNER set -g window-size manual || exit 1
|
||||
$INNER set -g automatic-rename off || exit 1
|
||||
$INNER set -g status-interval 0 || exit 1
|
||||
$INNER set -g mouse on || exit 1
|
||||
$INNER set -g pane-border-lines simple || exit 1
|
||||
FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5 \
|
||||
'printf FLOAT; exec sleep 100') || exit 1
|
||||
for terminal in "$FAST" "$SLOW"; do
|
||||
$terminal new-session -d -x 160 -y 80 'sleep 100' || exit 1
|
||||
$terminal set -g status off || exit 1
|
||||
$terminal set -g window-size manual || exit 1
|
||||
$terminal set -g default-terminal screen || exit 1
|
||||
$terminal respawn-pane -k "$INNER attach -t inner" || exit 1
|
||||
wait_marker "$terminal" READY
|
||||
done
|
||||
FASTCLIENT=$($FAST display -p '#{pane_tty}') || exit 1
|
||||
SLOWCLIENT=$($SLOW display -p '#{pane_tty}') || exit 1
|
||||
SLOWPID=$($SLOW display -p '#{pid}') || exit 1
|
||||
before=$($INNER display -p -c "$FASTCLIENT" '#{client_written}') || exit 1
|
||||
slowbefore=$($INNER display -p -c "$SLOWCLIENT" '#{client_written}') || exit 1
|
||||
# The large terminal keeps this backlog below the discard threshold, so
|
||||
# automatic recovery from discarded output cannot mask lost damage.
|
||||
# Stop only our outer server, leaving its inner client attached to a PTY
|
||||
# whose master is no longer read. This creates real terminal backpressure.
|
||||
STOPPED=$SLOWPID
|
||||
kill -STOP "$SLOWPID" || exit 1
|
||||
: >"$DIR/trigger-flood"
|
||||
i=0
|
||||
while :; do
|
||||
written=$($INNER display -p -c "$SLOWCLIENT" '#{client_written}') || exit 1
|
||||
fastwritten=$($INNER display -p -c "$FASTCLIENT" '#{client_written}') || exit 1
|
||||
# More than a PTY can buffer has been queued for the stopped terminal,
|
||||
# while the other terminal is still receiving the same output.
|
||||
[ "$written" -gt "$((slowbefore + 65536))" ] &&
|
||||
[ "$fastwritten" -gt "$((before + 65536))" ] && break
|
||||
[ "$i" -lt 100 ] || fail "slow client did not become blocked"
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
: >"$DIR/trigger-stop"
|
||||
wait_marker "$FAST" DONE
|
||||
[ "$($INNER display -p -c "$FASTCLIENT" '#{client_discarded}')" -eq 0 ] ||
|
||||
fail "fast client also became blocked"
|
||||
mouse 0 12 6 M
|
||||
mouse 32 42 6 M
|
||||
mouse 0 42 6 m
|
||||
[ "$($INNER display -p -t "$FLOAT" '#{pane_left}')" -eq 36 ] || fail "pane did not move"
|
||||
$FAST capture-pane -p >"$DIR/fast-before" || exit 1
|
||||
# Ensure the fast client already restored the vacated frame.
|
||||
sed -n '6p' "$DIR/fast-before" | grep -q '^FINAL06-0123456789' ||
|
||||
fail "fast client did not restore old footprint"
|
||||
kill -CONT "$SLOWPID" || exit 1
|
||||
STOPPED=
|
||||
wait_marker "$SLOW" DONE
|
||||
# Allow queued terminal output and deferred redraws to drain.
|
||||
sleep 0.5
|
||||
[ "$($INNER display -p -c "$SLOWCLIENT" '#{client_discarded}')" -eq 0 ] ||
|
||||
fail "discard recovery could mask lost deferred damage"
|
||||
$SLOW capture-pane -p >"$DIR/slow-before" || exit 1
|
||||
diff -u "$DIR/fast-before" "$DIR/slow-before" || fail "slow client did not catch up"
|
||||
$INNER refresh-client -t "$FASTCLIENT" || exit 1
|
||||
sleep 0.2
|
||||
$FAST capture-pane -p >"$DIR/after" || exit 1
|
||||
diff -u "$DIR/fast-before" "$DIR/after" || fail "damage redraw differed from full redraw"
|
||||
exit 0
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Moving and interactively resizing floating panes must restore both clients,
|
||||
# including a smaller client panned horizontally and vertically.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
LC_ALL=C.UTF-8
|
||||
export PATH TERM LC_ALL
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
INNER="$TEST_TMUX -Lviewports-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lviewports-outer-$$ -f/dev/null"
|
||||
|
||||
cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
mouse()
|
||||
{
|
||||
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
|
||||
$OUTER send-keys -t wide:0.0 -l "$sequence" || exit 1
|
||||
sleep 0.2
|
||||
}
|
||||
assert_scene()
|
||||
{
|
||||
# Compare what each terminal actually received with a fresh full redraw.
|
||||
# Capture both first: refreshing one client must not repair the other.
|
||||
for target in wide small; do
|
||||
$OUTER capture-pane -p -t "$target:0.0" >"$DIR/$target-before" || exit 1
|
||||
done
|
||||
$INNER refresh-client -t "$WIDE" || exit 1
|
||||
$INNER refresh-client -t "$SMALL" || exit 1
|
||||
sleep 0.2
|
||||
for target in wide small; do
|
||||
$OUTER capture-pane -p -t "$target:0.0" >"$DIR/$target-after" || exit 1
|
||||
diff -u "$DIR/$target-before" "$DIR/$target-after" ||
|
||||
fail "$1: $target client differed from a full redraw"
|
||||
done
|
||||
}
|
||||
|
||||
cat >"$DIR/background.pl" <<'PERL'
|
||||
$| = 1;
|
||||
for my $row (1 .. 24) {
|
||||
printf "\e[%d;1HROW%02d-", $row, $row;
|
||||
print '0123456789' x 7;
|
||||
}
|
||||
sleep 100;
|
||||
PERL
|
||||
$INNER new-session -d -s inner -x 80 -y 24 "perl '$DIR/background.pl'" || exit 1
|
||||
$INNER set -g status off || exit 1
|
||||
$INNER set -g window-size manual || exit 1
|
||||
$INNER set -g automatic-rename off || exit 1
|
||||
$INNER set -g status-interval 0 || exit 1
|
||||
$INNER set -g mouse on || exit 1
|
||||
$INNER set -g default-command 'sleep 100' || exit 1
|
||||
$INNER set -g pane-border-lines simple || exit 1
|
||||
$OUTER new-session -d -s wide -x 80 -y 24 'sleep 100' || exit 1
|
||||
$OUTER set -g status off || exit 1
|
||||
$OUTER set -g window-size manual || exit 1
|
||||
$OUTER set -g default-terminal screen || exit 1
|
||||
$OUTER new-session -d -s small -x 40 -y 12 'sleep 100' || exit 1
|
||||
for target in wide small; do
|
||||
$OUTER respawn-pane -k -t "$target:0.0" "$INNER attach -t inner" || exit 1
|
||||
done
|
||||
i=0
|
||||
while [ "$($INNER list-clients | wc -l)" -ne 2 ]; do
|
||||
[ "$i" -lt 50 ] || fail "two clients did not attach"
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
WIDE=$($OUTER display -p -t wide:0.0 '#{pane_tty}') || exit 1
|
||||
SMALL=$($OUTER display -p -t small:0.0 '#{pane_tty}') || exit 1
|
||||
$INNER refresh-client -t "$SMALL" -R 20 || exit 1
|
||||
$INNER refresh-client -t "$SMALL" -D 6 || exit 1
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 16 -y 6 -X 25 -Y 9) || exit 1
|
||||
sleep 0.3
|
||||
assert_scene initial
|
||||
# Confirm that the two clients really have different viewports.
|
||||
[ "$(head -1 "$DIR/wide-before" | cut -c1-6)" = ROW01- ] || fail "wrong wide viewport"
|
||||
[ "$(head -1 "$DIR/small-before" | cut -c1-6)" != ROW01- ] || fail "small client was not panned"
|
||||
|
||||
# Meta-drag the body using move-pane -M, including a partially clipped position.
|
||||
mouse 8 30 12 M
|
||||
mouse 40 48 15 M
|
||||
[ "$($INNER display -p -t "$FLOAT" '#{pane_left}')" -eq 44 ] || fail "Meta-drag did not move pane"
|
||||
assert_scene move-right
|
||||
mouse 40 28 10 M
|
||||
assert_scene move-back
|
||||
mouse 8 28 10 m
|
||||
|
||||
# Ctrl-drag creates a new floating pane, then changes its size while held.
|
||||
mouse 16 52 14 M
|
||||
mouse 48 75 23 M
|
||||
NEW=$($INNER display -p '#{pane_id}') || exit 1
|
||||
[ "$NEW" != "$FLOAT" ] || fail "Ctrl-drag did not create a pane"
|
||||
[ "$($INNER display -p -t "$NEW" '#{pane_floating_flag}')" -eq 1 ] || fail "new pane is not floating"
|
||||
assert_scene create
|
||||
mouse 48 64 19 M
|
||||
[ "$($INNER display -p -t "$NEW" '#{pane_width}')" -eq 11 ] || fail "Ctrl-drag did not shrink pane"
|
||||
assert_scene shrink
|
||||
mouse 16 64 19 m
|
||||
assert_scene release
|
||||
exit 0
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# renumber-windows with more windows than fit above base-index
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
TMUX="$TEST_TMUX -LtestRWL$$"
|
||||
$TMUX kill-server 2>/dev/null
|
||||
|
||||
TMP=$(mktemp)
|
||||
CONF=$(mktemp)
|
||||
trap "rm -f $TMP $CONF" 0 1 15
|
||||
|
||||
cat <<EOF >$CONF
|
||||
set -g base-index 10
|
||||
set -g renumber-windows on
|
||||
EOF
|
||||
|
||||
$TMUX -f$CONF new -d -s a
|
||||
$TMUX neww -d -t a
|
||||
$TMUX neww -d -t a
|
||||
echo $($TMUX lsw -t a -F'#{window_index}') >$TMP
|
||||
(echo "10 11 12"|cmp -s - $TMP) || exit 1
|
||||
$TMUX killw -t a:11
|
||||
echo $($TMUX lsw -t a -F'#{window_index}') >$TMP
|
||||
(echo "10 11"|cmp -s - $TMP) || exit 1
|
||||
$TMUX kill-server 2>/dev/null
|
||||
|
||||
cat <<EOF >$CONF
|
||||
set -g base-index 2147483646
|
||||
set -g renumber-windows on
|
||||
EOF
|
||||
|
||||
$TMUX -f$CONF new -d -s a
|
||||
$TMUX neww -d -t a
|
||||
$TMUX neww -d -t a
|
||||
$TMUX neww -d -t a
|
||||
echo $($TMUX lsw -t a -F'#{window_index}') >$TMP
|
||||
(echo "0 1 2147483646 2147483647"|cmp -s - $TMP) || exit 1
|
||||
|
||||
# Three windows do not fit from 2147483646, so the indexes stay as they are.
|
||||
$TMUX killw -t a:2147483647
|
||||
echo $($TMUX lsw -t a -F'#{window_index}') >$TMP
|
||||
(echo "0 1 2147483646"|cmp -s - $TMP) || exit 1
|
||||
$TMUX kill-server 2>/dev/null
|
||||
|
||||
exit 0
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A scroll held back by a synchronized update may be sent to all clients while
|
||||
# a later client is being redrawn. An earlier client which cannot scroll the
|
||||
# pane itself must still end up with the right contents.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
LC_ALL=C.UTF-8
|
||||
export PATH TERM LC_ALL
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
TMUX_TMPDIR=$DIR
|
||||
export TMUX_TMPDIR
|
||||
|
||||
INNER="$TEST_TMUX -Li$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
|
||||
CONTROL=$DIR/control
|
||||
EMITTER=$DIR/emitter.pl
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
|
||||
wait_for_file()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ] && [ ! -e "$1" ]; do
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
[ -e "$1" ] || fail "$2"
|
||||
}
|
||||
|
||||
wait_for_clients()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
n=$($INNER list-clients 2>/dev/null | wc -l)
|
||||
[ "$n" -eq "$1" ] && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "client $1 did not attach"
|
||||
}
|
||||
|
||||
cat >"$EMITTER" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
my $control = $ENV{CONTROL};
|
||||
(my $dir = $control) =~ s{/[^/]+$}{};
|
||||
|
||||
my @rows;
|
||||
for my $row (1 .. 24) {
|
||||
push @rows, sprintf("INIT_ROW_%02d_", $row) . ('X' x 16);
|
||||
}
|
||||
syswrite STDOUT, "\e[H" . join("\r\n", @rows);
|
||||
|
||||
open my $painted, '>', "$dir/painted" or die "$dir/painted: $!\n";
|
||||
close $painted;
|
||||
while (!-e $control) {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
|
||||
# Start a synchronized update and scroll, but do not end it.
|
||||
syswrite STDOUT, "\e[?2026h\e[24;1H\r\nSCROLLED_LINE_";
|
||||
open my $scrolled, '>', "$dir/scrolled" or die "$dir/scrolled: $!\n";
|
||||
close $scrolled;
|
||||
select undef, undef, undef, 3;
|
||||
PERL
|
||||
|
||||
$INNER new-session -d -s inner -x 80 -y 24 \
|
||||
"CONTROL='$CONTROL' perl '$EMITTER'" || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
$INNER set-option -as terminal-features '*:sync' || exit 1
|
||||
|
||||
# The pane is not full width, so clients cannot scroll it.
|
||||
$INNER split-window -h -d -t inner:0.0 'sleep 30' || exit 1
|
||||
|
||||
for name in a b; do
|
||||
$OUTER new-session -d -s $name -x 80 -y 24 \
|
||||
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" || exit 1
|
||||
$OUTER set-option -t $name status off || exit 1
|
||||
if [ "$name" = a ]; then
|
||||
wait_for_clients 1
|
||||
fi
|
||||
done
|
||||
$OUTER set-option -g window-size manual || exit 1
|
||||
wait_for_clients 2
|
||||
B=$($OUTER display-message -p -t b: '#{pane_tty}') || exit 1
|
||||
|
||||
wait_for_file "$DIR/painted" "application did not paint"
|
||||
sleep 1
|
||||
: >"$CONTROL"
|
||||
wait_for_file "$DIR/scrolled" "application did not scroll"
|
||||
sleep 0.2
|
||||
|
||||
# Redraw only the second client while the update is still in progress.
|
||||
$INNER refresh-client -t "$B" || exit 1
|
||||
sleep 1
|
||||
|
||||
$OUTER capture-pane -p -t a: >"$DIR/a" || exit 1
|
||||
$OUTER capture-pane -p -t b: >"$DIR/b" || exit 1
|
||||
sed -n 1p "$DIR/b" | grep -q '^INIT_ROW_02_' ||
|
||||
fail "second client not scrolled"
|
||||
sed -n 24p "$DIR/b" | grep -q '^SCROLLED_LINE_' ||
|
||||
fail "second client missing scrolled line"
|
||||
cmp -s "$DIR/a" "$DIR/b" || fail "first client differs from second client"
|
||||
exit 0
|
||||
@@ -1,150 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Check that synchronized scrolling does not redraw unchanged lines.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
LC_ALL=C.UTF-8
|
||||
export PATH TERM LC_ALL
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
TMUX_TMPDIR=$DIR
|
||||
export TMUX_TMPDIR
|
||||
|
||||
INNER="$TEST_TMUX -Li$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
|
||||
CLIENT_BYTES=$DIR/client-bytes
|
||||
CONTROL=$DIR/control
|
||||
EMITTER=$DIR/emitter.pl
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
|
||||
wait_for_file()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ] && [ ! -e "$1" ]; do
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
[ -e "$1" ] || fail "$2"
|
||||
}
|
||||
|
||||
wait_for_client()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$INNER list-clients -F '#{client_termfeatures}' 2>/dev/null |
|
||||
grep -q 'sync' && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "sync-capable client did not attach"
|
||||
}
|
||||
|
||||
wait_for_stable_bytes()
|
||||
{
|
||||
previous=-1
|
||||
stable=0
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
current=$(wc -c <"$CLIENT_BYTES" 2>/dev/null) || current=0
|
||||
if [ "$current" -gt 0 ] && [ "$current" -eq "$previous" ]; then
|
||||
stable=$((stable + 1))
|
||||
[ "$stable" -eq 5 ] && return 0
|
||||
else
|
||||
stable=0
|
||||
fi
|
||||
previous=$current
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "client byte stream did not become stable"
|
||||
}
|
||||
|
||||
cat >"$EMITTER" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
my $control = $ENV{CONTROL};
|
||||
(my $dir = $control) =~ s{/[^/]+$}{};
|
||||
|
||||
# Fill the screen outside a synchronized update.
|
||||
my @rows;
|
||||
for my $row (1 .. 24) {
|
||||
push @rows, substr(sprintf("INIT_ROW_%02d_", $row) . ('X' x 79), 0, 79);
|
||||
}
|
||||
syswrite STDOUT, "\e[H" . join("\r\n", @rows);
|
||||
|
||||
open my $painted, '>', "$dir/painted" or die "$dir/painted: $!\n";
|
||||
close $painted;
|
||||
while (!-e $control) {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
|
||||
# Scroll one line and overwrite text in a synchronized update.
|
||||
my $frame = "\e[?2026h" .
|
||||
"\e[24;1H\r\nSCROLLED_LINE_" .
|
||||
"\e[23;60HCHANGED_" .
|
||||
"\e[?2026l";
|
||||
my $written = syswrite STDOUT, $frame;
|
||||
die "short synchronized frame write\n"
|
||||
unless defined $written && $written == length $frame;
|
||||
select undef, undef, undef, 3;
|
||||
PERL
|
||||
|
||||
$INNER new-session -d -s inner -x 80 -y 24 \
|
||||
"CONTROL='$CONTROL' perl '$EMITTER'" || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
$INNER set-option -as terminal-features '*:sync' || exit 1
|
||||
|
||||
$OUTER new-session -d -s outer -x 80 -y 24 \
|
||||
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" ||
|
||||
exit 1
|
||||
$OUTER set-option -g status off || exit 1
|
||||
$OUTER set-option -g window-size manual || exit 1
|
||||
wait_for_client
|
||||
|
||||
wait_for_file "$DIR/painted" "application did not paint"
|
||||
sleep 1
|
||||
$OUTER pipe-pane -O -t outer:0.0 "cat >'$CLIENT_BYTES'" || exit 1
|
||||
: >"$CONTROL"
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
grep -q 'SCROLLED_LINE_' "$CLIENT_BYTES" 2>/dev/null && break
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
grep -q 'SCROLLED_LINE_' "$CLIENT_BYTES" || fail "scrolled line not sent"
|
||||
wait_for_stable_bytes
|
||||
$OUTER pipe-pane -t outer:0.0 || exit 1
|
||||
|
||||
# Check that only changed lines are redrawn.
|
||||
grep -q 'CHANGED_' "$CLIENT_BYTES" || fail "changed cell not sent"
|
||||
if grep -q 'INIT_ROW_05_' "$CLIENT_BYTES"; then
|
||||
fail "unchanged line redrawn after synchronized scroll"
|
||||
fi
|
||||
|
||||
# The client must show the scrolled screen.
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$DIR/screen" || exit 1
|
||||
sed -n 1p "$DIR/screen" | grep -q '^INIT_ROW_02_' ||
|
||||
fail "first line not scrolled"
|
||||
sed -n 23p "$DIR/screen" | grep -q 'INIT_ROW_24_.*CHANGED_' ||
|
||||
fail "changed cell not on screen"
|
||||
sed -n 24p "$DIR/screen" | grep -q '^SCROLLED_LINE_' ||
|
||||
fail "scrolled line not on screen"
|
||||
exit 0
|
||||
@@ -1,7 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
# With left/right margins, a pane narrowed by a scrollbar can scroll without
|
||||
# retransmitting its existing rows. Both paths must produce the same screen.
|
||||
# A pane that doesn't span the terminal's full width - because
|
||||
# pane-scrollbars is on (the scrollbar occupies a column) or the pane is
|
||||
# one of a side-by-side split - needs DECSLRM (left/right margin) support
|
||||
# to use the fast native-scroll path (tty_cmd_linefeed()/scrollup()/
|
||||
# scrolldown()/reverseindex(), tty.c:
|
||||
# "(!tty_full_width(tty, ctx) && !tty_use_margin(tty))"). Without it, every
|
||||
# single scroll falls back to tty_redraw_region()'s full manual repaint of
|
||||
# the whole region - a real, confirmed source of flicker (and, separately,
|
||||
# of image content not surviving a scroll in branches with image support).
|
||||
#
|
||||
# tty_default_features() (tty-features.c) grants the "margins" feature to
|
||||
# several terminals it can positively identify via XTVERSION/DA2 (mintty,
|
||||
# iTerm2, WezTerm, ghostty, XTerm-as-VT420) - this checks the actual
|
||||
# server-side scroll decision via the -vv log for the underlying mechanism
|
||||
# those table entries all rely on, using the terminal-features option
|
||||
# directly (which any of them - or a user's own terminal-overrides -
|
||||
# ultimately feed into) rather than simulating any one terminal's
|
||||
# identification handshake.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -11,7 +27,8 @@ export PATH TERM LC_ALL
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
INNER="$TEST_TMUX -Lmarginsscrollbar-inner-$$ -f/dev/null"
|
||||
cd "$DIR" || exit 1
|
||||
INNER="$TEST_TMUX -vv -Lmarginsscrollbar-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lmarginsscrollbar-outer-$$ -f/dev/null"
|
||||
|
||||
fail()
|
||||
@@ -24,70 +41,77 @@ cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
cd /
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
|
||||
cat >"$DIR/emitter.pl" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
$| = 1;
|
||||
print join("\r\n", map { "KEEP0$_" } 1 .. 6);
|
||||
while (!-e $ENV{TRIGGER}) {
|
||||
select undef, undef, undef, 0.01;
|
||||
}
|
||||
print "\r\nNEWROW";
|
||||
sleep 100;
|
||||
PERL
|
||||
|
||||
for margins in on off; do
|
||||
$INNER new-session -d -s inner -x 40 -y 6 \
|
||||
"TRIGGER='$DIR/trigger-$margins' perl '$DIR/emitter.pl'" || exit 1
|
||||
$INNER set -g status off || exit 1
|
||||
$INNER set -g status-interval 0 || exit 1
|
||||
$INNER set -g automatic-rename off || exit 1
|
||||
$INNER set -g window-size manual || exit 1
|
||||
$INNER set -g pane-scrollbars on || exit 1
|
||||
if [ "$margins" = on ]; then
|
||||
$INNER set -as terminal-features ',screen-256color:margins' || exit 1
|
||||
fi
|
||||
|
||||
$OUTER new-session -d -s outer -x 40 -y 6 'sleep 100' || exit 1
|
||||
$OUTER set -g status off || exit 1
|
||||
$OUTER set -g window-size manual || exit 1
|
||||
$OUTER set -g default-terminal screen-256color || exit 1
|
||||
$OUTER respawn-pane -k -t outer:0.0 "$INNER attach -t inner" || exit 1
|
||||
sleep 0.5
|
||||
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output-$margins'" || exit 1
|
||||
$INNER refresh-client || exit 1
|
||||
sleep 0.5
|
||||
grep -aq KEEP02 "$DIR/output-$margins" || fail "initial rows not captured"
|
||||
offset=$(wc -c <"$DIR/output-$margins")
|
||||
: >"$DIR/trigger-$margins"
|
||||
wait_for_client()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$DIR/screen-$margins" || exit 1
|
||||
grep -q NEWROW "$DIR/screen-$margins" && break
|
||||
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
|
||||
[ -n "$CLIENT" ] && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
[ "$i" -lt 50 ] || fail "$margins: scroll did not reach the terminal"
|
||||
sleep 0.2
|
||||
tail -c +"$((offset + 1))" "$DIR/output-$margins" >"$DIR/scroll-$margins"
|
||||
grep -aq NEWROW "$DIR/scroll-$margins" || fail "scroll output not captured"
|
||||
if [ "$margins" = on ]; then
|
||||
if grep -aq KEEP02 "$DIR/scroll-$margins"; then
|
||||
fail "scroll with margins retransmitted an existing row"
|
||||
fi
|
||||
else
|
||||
grep -aq KEEP02 "$DIR/scroll-$margins" ||
|
||||
fail "scroll without margins did not exercise the redraw fallback"
|
||||
fail "inner client did not attach"
|
||||
}
|
||||
|
||||
run_scroll_phase()
|
||||
{
|
||||
label=$1
|
||||
margins=$2
|
||||
|
||||
rm -f tmux-server*.log
|
||||
|
||||
$INNER new-session -d -s inner -x 40 -y 6 'exec sh' || exit 1
|
||||
$INNER set -g status off || exit 1
|
||||
$INNER set -g window-size manual || exit 1
|
||||
$INNER set -g pane-scrollbars on || exit 1
|
||||
if [ "$margins" = "on" ]; then
|
||||
$INNER set -as terminal-features ',*:margins' || exit 1
|
||||
fi
|
||||
printf 'KEEP02\nKEEP03\nKEEP04\nKEEP05\nKEEP06\nNEWROW\n' >"$DIR/expected"
|
||||
cmp -s "$DIR/expected" "$DIR/screen-$margins" ||
|
||||
fail "$margins: terminal did not contain the expected scrolled rows"
|
||||
|
||||
$OUTER new-session -d -x 40 -y 6 || exit 1
|
||||
OUTERPANE=$($OUTER list-panes -F '#{pane_id}') || exit 1
|
||||
$OUTER set -g status off || exit 1
|
||||
$OUTER set -g window-size manual || exit 1
|
||||
$OUTER set -g default-terminal screen-256color || exit 1
|
||||
$OUTER send-keys -t "$OUTERPANE" -l "$INNER attach -t inner" || exit 1
|
||||
$OUTER send-keys -t "$OUTERPANE" Enter || exit 1
|
||||
sleep 1
|
||||
|
||||
wait_for_client
|
||||
|
||||
i=0
|
||||
while [ "$i" -lt 8 ]; do
|
||||
$INNER send-keys -t inner Enter || exit 1
|
||||
sleep 0.2
|
||||
i=$((i + 1))
|
||||
done
|
||||
sleep 0.3
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "$label: sanity: no server -vv log was produced"
|
||||
|
||||
n=$(grep -c "tty_redraw_region.*large region redraw" "$LOG")
|
||||
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
done
|
||||
|
||||
echo "$n"
|
||||
}
|
||||
|
||||
# Phase 1: margins granted - must never fall back to a full region redraw.
|
||||
n_with=$(run_scroll_phase "with margins" "on")
|
||||
[ "$n_with" -eq 0 ] ||
|
||||
fail "with margins granted, scrolling a scrollbar-enabled pane still fell back to a full region redraw ($n_with times)"
|
||||
|
||||
# Phase 2: sanity check - without margins, the same scenario must actually
|
||||
# hit the fallback, proving phase 1 wasn't accidentally trivial.
|
||||
n_without=$(run_scroll_phase "without margins" "off")
|
||||
[ "$n_without" -gt 0 ] ||
|
||||
fail "sanity: without margins, scrolling a scrollbar-enabled pane never fell back to a full region redraw - this scenario no longer exercises the bug this test checks for"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# inserting a window next to the highest index
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
TMUX="$TEST_TMUX -LtestWIL$$ -f/dev/null"
|
||||
$TMUX kill-server 2>/dev/null
|
||||
|
||||
TMP=$(mktemp)
|
||||
trap "rm -f $TMP" 0 1 15
|
||||
|
||||
$TMUX new -d -s one
|
||||
$TMUX set -g base-index 2147483647
|
||||
$TMUX neww -d -t one
|
||||
|
||||
$TMUX neww -d -a -t one:2147483647
|
||||
$TMUX neww -d -b -t one:2147483647
|
||||
|
||||
echo $($TMUX lsw -t one -F'#{window_index}') >$TMP
|
||||
(echo "0 2147483647"|cmp -s - $TMP) || exit 1
|
||||
$TMUX kill-server 2>/dev/null
|
||||
|
||||
exit 0
|
||||
@@ -152,7 +152,18 @@ clients_calculate_size(int type, int current, struct client *c,
|
||||
*sx = UINT_MAX;
|
||||
*sy = UINT_MAX;
|
||||
}
|
||||
*xpixel = *ypixel = 0;
|
||||
if (w != NULL && type == WINDOW_SIZE_MANUAL) {
|
||||
/*
|
||||
* Manual mode has no separate pixel size to apply, so keep the
|
||||
* window's own - otherwise this always differs from the real
|
||||
* w->xpixel/ypixel, and recalculate_size() can never tell a
|
||||
* later call apart from an actual change, resizing (and
|
||||
* re-firing window-resized) on every recalculation.
|
||||
*/
|
||||
*xpixel = w->xpixel;
|
||||
*ypixel = w->ypixel;
|
||||
} else
|
||||
*xpixel = *ypixel = 0;
|
||||
|
||||
/*
|
||||
* For latest, count the number of clients with this window. We only
|
||||
@@ -386,10 +397,12 @@ recalculate_size(struct window *w, int now)
|
||||
* got a resize scheduled, then use the new size; otherwise the old.
|
||||
*/
|
||||
if (w->flags & WINDOW_RESIZE) {
|
||||
if (!now && changed && w->new_sx == sx && w->new_sy == sy)
|
||||
if (!now && changed && w->new_sx == sx && w->new_sy == sy &&
|
||||
w->new_xpixel == xpixel && w->new_ypixel == ypixel)
|
||||
changed = 0;
|
||||
} else {
|
||||
if (!now && changed && w->sx == sx && w->sy == sy)
|
||||
if (!now && changed && w->sx == sx && w->sy == sy &&
|
||||
w->xpixel == xpixel && w->ypixel == ypixel)
|
||||
changed = 0;
|
||||
}
|
||||
|
||||
|
||||
+663
-135
File diff suppressed because it is too large
Load Diff
+117
-261
@@ -1,4 +1,4 @@
|
||||
/* $OpenBSD: screen-write.c,v 1.293 2026/09/24 11:19:39 nicm Exp $ */
|
||||
/* $OpenBSD: screen-write.c,v 1.292 2026/09/21 12:14:32 nicm Exp $ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||
@@ -34,13 +34,15 @@ static void screen_write_collect_clear(struct screen_write_ctx *, u_int,
|
||||
static void screen_write_collect_scroll(struct screen_write_ctx *, u_int);
|
||||
static void screen_write_collect_flush(struct screen_write_ctx *, int,
|
||||
const char *);
|
||||
#ifdef ENABLE_IMAGES
|
||||
static void screen_write_image_damage(struct screen_write_ctx *, u_int,
|
||||
u_int, u_int, u_int);
|
||||
#endif
|
||||
static int screen_write_overwrite(struct screen_write_ctx *,
|
||||
struct grid_cell *, u_int);
|
||||
static int screen_write_combine(struct screen_write_ctx *,
|
||||
const struct grid_cell *);
|
||||
static void screen_write_sync_flush_dirty(struct window_pane *);
|
||||
static void screen_write_sync_apply_scroll(struct screen_write_ctx *,
|
||||
struct tty_ctx *);
|
||||
static void screen_write_flush_dirty(struct window_pane *);
|
||||
|
||||
struct screen_write_citem {
|
||||
u_int x;
|
||||
@@ -122,7 +124,18 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy)
|
||||
evtimer_add(&w->offset_timer, &tv);
|
||||
}
|
||||
|
||||
/* Redraw lines. */
|
||||
/*
|
||||
* Called when a write could not be applied directly to the terminal and
|
||||
* needs a redraw instead. Report damage for the requested rows. wp->yoff is
|
||||
* already adjusted past any top pane-border-status row, so wp->yoff + py is
|
||||
* the correct window-coordinate row. wp->xoff/wp->yoff are signed and can be
|
||||
* negative for a floating pane positioned partly off the window's left or
|
||||
* top edge, so clip to the window's own origin here before converting to
|
||||
* the unsigned coordinates redraw_damage_window() takes - passing a
|
||||
* negative offset through unclipped wraps to a huge value that its own
|
||||
* bounds check then silently rejects, losing the pane's visible portion
|
||||
* entirely rather than just the off-screen part.
|
||||
*/
|
||||
static void
|
||||
screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny)
|
||||
{
|
||||
@@ -140,8 +153,10 @@ screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny)
|
||||
x0 = 0;
|
||||
if (y0 < 0)
|
||||
y0 = 0;
|
||||
if (x1 > x0 && y1 > y0)
|
||||
redraw_damage_window(wp->window, x0, y0, x1 - x0, y1 - y0);
|
||||
if (x1 <= x0 || y1 <= y0)
|
||||
return;
|
||||
redraw_damage_window(wp->window, (u_int)x0, (u_int)y0,
|
||||
(u_int)(x1 - x0), (u_int)(y1 - y0));
|
||||
}
|
||||
|
||||
/* Update context for client. */
|
||||
@@ -229,27 +244,6 @@ screen_write_pane_is_obscured(struct screen_write_ctx *ctx)
|
||||
return (0);
|
||||
}
|
||||
|
||||
/* Allocate dirty lines for this screen size. */
|
||||
static bitstr_t *
|
||||
screen_write_sync_allocate_dirty(struct window_pane *wp, u_int sy)
|
||||
{
|
||||
bitstr_t *bs = wp->sync_dirty;
|
||||
|
||||
if (bs != NULL && wp->sync_dirty_size == sy)
|
||||
return (bs);
|
||||
|
||||
free(bs);
|
||||
bs = wp->sync_dirty = bit_alloc(sy);
|
||||
if (bs == NULL)
|
||||
fatal("bit_alloc failed");
|
||||
if (wp->sync_dirty_size != 0) {
|
||||
bit_nset(bs, 0, sy - 1);
|
||||
wp->sync_scrolled = 0;
|
||||
}
|
||||
wp->sync_dirty_size = sy;
|
||||
return (bs);
|
||||
}
|
||||
|
||||
/* Should we draw to the TTY? */
|
||||
static int
|
||||
screen_write_should_draw_lines(struct screen_write_ctx *ctx, u_int y, u_int ny)
|
||||
@@ -263,9 +257,21 @@ screen_write_should_draw_lines(struct screen_write_ctx *ctx, u_int y, u_int ny)
|
||||
return (0);
|
||||
if (s->mode & MODE_SYNC) {
|
||||
if (wp != NULL && y < sy && ny != 0) {
|
||||
bs = wp->sync_dirty;
|
||||
if (ny > sy - y)
|
||||
ny = sy - y;
|
||||
bs = screen_write_sync_allocate_dirty(wp, sy);
|
||||
if (bs == NULL || wp->sync_dirty_size != sy) {
|
||||
if (bs != NULL && wp->sync_dirty_size != sy) {
|
||||
y = 0;
|
||||
ny = sy;
|
||||
}
|
||||
free(bs);
|
||||
|
||||
bs = wp->sync_dirty = bit_alloc(sy);
|
||||
if (bs == NULL)
|
||||
fatal("bit_alloc failed");
|
||||
wp->sync_dirty_size = sy;
|
||||
}
|
||||
bit_nset(bs, y, y + ny - 1);
|
||||
}
|
||||
return (0);
|
||||
@@ -389,6 +395,19 @@ screen_write_init(struct screen_write_ctx *ctx, struct screen *s)
|
||||
ctx->bg = 8;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
/* Damage temporal images when application input writes text cells. */
|
||||
static void
|
||||
screen_write_image_damage(struct screen_write_ctx *ctx, u_int x, u_int y,
|
||||
u_int sx, u_int sy)
|
||||
{
|
||||
image_redraw_area(ctx, x, y, sx, sy);
|
||||
if (ctx->flags & SCREEN_WRITE_INPUT)
|
||||
image_grid_damage(ctx->s->grid, x, ctx->s->grid->hsize + y,
|
||||
sx, sy);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Initialize writing with a pane. */
|
||||
void
|
||||
screen_write_start_pane(struct screen_write_ctx *ctx, struct window_pane *wp,
|
||||
@@ -1055,7 +1074,7 @@ screen_write_sync_callback(__unused int fd, __unused short events, void *arg)
|
||||
|
||||
if (wp->base.mode & MODE_SYNC) {
|
||||
wp->base.mode &= ~MODE_SYNC;
|
||||
screen_write_sync_flush_dirty(wp);
|
||||
screen_write_flush_dirty(wp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1087,7 +1106,7 @@ screen_write_stop_sync(struct window_pane *wp)
|
||||
evtimer_del(&wp->sync_timer);
|
||||
wp->base.mode &= ~MODE_SYNC;
|
||||
|
||||
screen_write_sync_flush_dirty(wp);
|
||||
screen_write_flush_dirty(wp);
|
||||
|
||||
log_debug("%s: %%%u stopped sync mode", __func__, wp->id);
|
||||
}
|
||||
@@ -1295,7 +1314,7 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
|
||||
|
||||
/* Redraw dirty lines. */
|
||||
static void
|
||||
screen_write_sync_flush_dirty(struct window_pane *wp)
|
||||
screen_write_flush_dirty(struct window_pane *wp)
|
||||
{
|
||||
struct screen_write_ctx ctx;
|
||||
struct tty_ctx ttyctx;
|
||||
@@ -1308,143 +1327,29 @@ screen_write_sync_flush_dirty(struct window_pane *wp)
|
||||
screen_write_start_pane(&ctx, wp, s);
|
||||
screen_write_initctx(&ctx, &ttyctx, 1, 1);
|
||||
|
||||
if (wp->sync_scrolled != 0)
|
||||
screen_write_sync_apply_scroll(&ctx, &ttyctx);
|
||||
|
||||
if (~wp->flags & PANE_REDRAW) {
|
||||
for (y = 0; y < sy; y++) {
|
||||
if (bit_test(wp->sync_dirty, y)) {
|
||||
screen_write_redraw_line(&ctx, &ttyctx, y);
|
||||
lines++;
|
||||
}
|
||||
for (y = 0; y < sy; y++) {
|
||||
if (bit_test(wp->sync_dirty, y)) {
|
||||
screen_write_redraw_line(&ctx, &ttyctx, y);
|
||||
lines++;
|
||||
}
|
||||
}
|
||||
log_debug("%s: %%%u had %u dirty lines", __func__, wp->id, lines);
|
||||
|
||||
screen_write_stop(&ctx);
|
||||
screen_write_sync_clear_dirty(wp);
|
||||
screen_write_clear_dirty(wp);
|
||||
}
|
||||
|
||||
/* Clear pending synchronized output. */
|
||||
/* Clear any dirty lines. */
|
||||
void
|
||||
screen_write_sync_clear_dirty(struct window_pane *wp)
|
||||
screen_write_clear_dirty(struct window_pane *wp)
|
||||
{
|
||||
if (wp != NULL) {
|
||||
if (wp != NULL && wp->sync_dirty != NULL) {
|
||||
free(wp->sync_dirty);
|
||||
wp->sync_dirty = NULL;
|
||||
wp->sync_dirty_size = 0;
|
||||
wp->sync_scrolled = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Defer scrolling until the synchronized update ends. */
|
||||
static void
|
||||
screen_write_sync_scroll_dirty(struct screen_write_ctx *ctx)
|
||||
{
|
||||
struct window_pane *wp = ctx->wp;
|
||||
struct screen *s = ctx->s;
|
||||
u_int n = ctx->scrolled, y, sy = screen_size_y(s);
|
||||
u_int ry = s->rlower + 1 - s->rupper;
|
||||
bitstr_t *bs;
|
||||
|
||||
if (n > ry)
|
||||
n = ry;
|
||||
if (wp == NULL ||
|
||||
n == ry ||
|
||||
s->rlower >= sy ||
|
||||
window_pane_scrollbar_overlay_visible(wp)) {
|
||||
screen_write_should_draw_lines(ctx, s->rupper, ry);
|
||||
return;
|
||||
}
|
||||
if (wp->flags & (PANE_REDRAW|PANE_DROP))
|
||||
return;
|
||||
bs = screen_write_sync_allocate_dirty(wp, sy);
|
||||
|
||||
if (wp->sync_scrolled != 0 &&
|
||||
(wp->sync_rupper != s->rupper ||
|
||||
wp->sync_rlower != s->rlower ||
|
||||
wp->sync_bg != ctx->bg)) {
|
||||
log_debug("%s: %%%u region changed, redrawing all", __func__,
|
||||
wp->id);
|
||||
bit_nset(bs, 0, sy - 1);
|
||||
wp->sync_scrolled = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Keep dirty lines aligned with the scrolled grid. */
|
||||
for (y = s->rupper; y + n <= s->rlower; y++) {
|
||||
if (bit_test(bs, y + n))
|
||||
bit_set(bs, y);
|
||||
else
|
||||
bit_clear(bs, y);
|
||||
}
|
||||
bit_nset(bs, s->rlower + 1 - n, s->rlower);
|
||||
|
||||
wp->sync_rupper = s->rupper;
|
||||
wp->sync_rlower = s->rlower;
|
||||
wp->sync_bg = ctx->bg;
|
||||
wp->sync_scrolled += n;
|
||||
if (wp->sync_scrolled >= ry) {
|
||||
bit_nset(bs, s->rupper, s->rlower);
|
||||
wp->sync_scrolled = 0;
|
||||
}
|
||||
window_pane_scrollbar_redraw(wp);
|
||||
|
||||
log_debug("%s: %%%u deferred scroll of %u (region %u-%u)", __func__,
|
||||
wp->id, wp->sync_scrolled, s->rupper, s->rlower);
|
||||
}
|
||||
|
||||
/* Redraw the scrolled lines for a client which cannot scroll them. */
|
||||
static void
|
||||
screen_write_sync_redraw_cb(const struct tty_ctx *ttyctx)
|
||||
{
|
||||
struct window_pane *wp = ttyctx->arg;
|
||||
|
||||
bit_nset(wp->sync_dirty, wp->sync_rupper, wp->sync_rlower);
|
||||
}
|
||||
|
||||
/* Send the deferred scroll to the client. */
|
||||
static void
|
||||
screen_write_sync_apply_scroll(struct screen_write_ctx *ctx,
|
||||
struct tty_ctx *ttyctx)
|
||||
{
|
||||
struct window_pane *wp = ctx->wp;
|
||||
struct screen *s = ctx->s;
|
||||
u_int sy = screen_size_y(s), n = wp->sync_scrolled;
|
||||
tty_ctx_redraw_cb redraw_cb;
|
||||
|
||||
wp->sync_scrolled = 0;
|
||||
if (wp->flags & PANE_REDRAW)
|
||||
return;
|
||||
if (wp->sync_rlower >= sy || wp->sync_rupper > wp->sync_rlower) {
|
||||
bit_nset(wp->sync_dirty, 0, sy - 1);
|
||||
return;
|
||||
}
|
||||
if ((ttyctx->flags & TTY_CTX_PANE_OBSCURED) ||
|
||||
window_pane_scrollbar_overlay_visible(wp)) {
|
||||
bit_nset(wp->sync_dirty, wp->sync_rupper, wp->sync_rlower);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("%s: %%%u scroll of %u (region %u-%u)", __func__, wp->id, n,
|
||||
wp->sync_rupper, wp->sync_rlower);
|
||||
|
||||
ttyctx->orupper = wp->sync_rupper;
|
||||
ttyctx->orlower = wp->sync_rlower;
|
||||
if (wp->yoff + wp->sy > wp->window->sy)
|
||||
ttyctx->orlower -= (wp->yoff + wp->sy - wp->window->sy);
|
||||
ttyctx->n = n;
|
||||
ttyctx->bg = wp->sync_bg;
|
||||
redraw_cb = ttyctx->redraw_cb;
|
||||
ttyctx->redraw_cb = screen_write_sync_redraw_cb;
|
||||
tty_write(tty_cmd_scrollup, ttyctx);
|
||||
|
||||
ttyctx->redraw_cb = redraw_cb;
|
||||
ttyctx->orupper = s->rupper;
|
||||
ttyctx->orlower = s->rlower;
|
||||
ttyctx->n = 0;
|
||||
}
|
||||
|
||||
/* Redraw all visible cells in a pane. */
|
||||
static void
|
||||
screen_write_redraw_pane(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
|
||||
@@ -1468,9 +1373,11 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx)
|
||||
memcpy(&gc, &grid_default_cell, sizeof gc);
|
||||
utf8_set(&gc.data, 'E');
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_free_all(s) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_all(ctx);
|
||||
if (ctx->flags & SCREEN_WRITE_INPUT)
|
||||
image_grid_damage(s->grid, 0, s->grid->hsize,
|
||||
screen_size_x(s), screen_size_y(s));
|
||||
#endif
|
||||
|
||||
for (yy = 0; yy < screen_size_y(s); yy++) {
|
||||
@@ -1515,9 +1422,8 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
|
||||
if (s->cx > screen_size_x(s) - 1)
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
|
||||
#endif
|
||||
|
||||
screen_write_initctx(ctx, &ttyctx, 0, 1);
|
||||
@@ -1556,9 +1462,8 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
|
||||
if (s->cx > screen_size_x(s) - 1)
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
|
||||
#endif
|
||||
|
||||
screen_write_initctx(ctx, &ttyctx, 0, 1);
|
||||
@@ -1597,9 +1502,8 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
|
||||
if (s->cx > screen_size_x(s) - 1)
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
|
||||
#endif
|
||||
|
||||
screen_write_initctx(ctx, &ttyctx, 0, 1);
|
||||
@@ -1632,9 +1536,8 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
|
||||
if (ny == 0)
|
||||
ny = 1;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
|
||||
#endif
|
||||
|
||||
if (s->cy < s->rupper || s->cy > s->rlower) {
|
||||
@@ -1700,9 +1603,8 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
|
||||
if (ny == 0)
|
||||
ny = 1;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
|
||||
#endif
|
||||
|
||||
if (s->cy < s->rupper || s->cy > s->rlower) {
|
||||
@@ -1773,9 +1675,8 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg)
|
||||
if (gl->cellsize == 0 && COLOUR_DEFAULT(bg))
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
|
||||
#endif
|
||||
|
||||
flags = gl->flags & GRID_LINE_OSC133_FLAGS;
|
||||
@@ -1812,9 +1713,8 @@ screen_write_clearendofline(struct screen_write_ctx *ctx, u_int bg)
|
||||
if (s->cx > sx - 1 || (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg)))
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
|
||||
#endif
|
||||
|
||||
grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg);
|
||||
@@ -1839,9 +1739,8 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
|
||||
#endif
|
||||
|
||||
if (s->cx > sx - 1)
|
||||
@@ -1893,9 +1792,8 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_free_all(s) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_scroll(ctx, 1);
|
||||
#endif
|
||||
|
||||
grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg);
|
||||
@@ -1945,9 +1843,6 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
|
||||
struct screen *s = ctx->s;
|
||||
struct grid *gd = s->grid;
|
||||
struct grid_line *gl;
|
||||
#ifdef ENABLE_SIXEL
|
||||
int redraw = 0;
|
||||
#endif
|
||||
u_int rupper = s->rupper, rlower = s->rlower;
|
||||
|
||||
gl = grid_get_line(gd, gd->hsize + s->cy);
|
||||
@@ -1968,13 +1863,12 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (rlower == screen_size_y(s) - 1)
|
||||
redraw = image_scroll_up(s, 1);
|
||||
image_redraw_scroll(ctx, 1);
|
||||
else
|
||||
redraw = image_check_line(s, rupper, rlower - rupper);
|
||||
if (redraw && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
image_redraw_area(ctx, 0, rupper, screen_size_x(s),
|
||||
rlower - rupper);
|
||||
#endif
|
||||
|
||||
grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg);
|
||||
@@ -2000,9 +1894,8 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg)
|
||||
ctx->bg = bg;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_scroll_up(s, lines) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_scroll(ctx, lines);
|
||||
#endif
|
||||
|
||||
for (i = 0; i < lines; i++) {
|
||||
@@ -2029,9 +1922,8 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg)
|
||||
else if (lines > s->rlower - s->rupper + 1)
|
||||
lines = s->rlower - s->rupper + 1;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_free_all(s) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_scroll(ctx, lines);
|
||||
#endif
|
||||
|
||||
for (i = 0; i < lines; i++)
|
||||
@@ -2070,9 +1962,8 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg)
|
||||
struct visible_ranges *r;
|
||||
struct visible_range *ri;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
|
||||
#endif
|
||||
|
||||
screen_write_initctx(ctx, &ttyctx, 1, 1);
|
||||
@@ -2151,9 +2042,8 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg)
|
||||
struct visible_ranges *r;
|
||||
struct visible_range *ri;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_line(s, 0, s->cy - 1) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_area(ctx, 0, 0, screen_size_x(s), s->cy + 1);
|
||||
#endif
|
||||
|
||||
screen_write_initctx(ctx, &ttyctx, 1, 1);
|
||||
@@ -2224,9 +2114,8 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
|
||||
struct visible_ranges *r;
|
||||
struct visible_range *ri;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_free_all(s) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_redraw_all(ctx);
|
||||
#endif
|
||||
|
||||
screen_write_initctx(ctx, &ttyctx, 1, 1);
|
||||
@@ -2564,8 +2453,10 @@ screen_write_collect_flush(struct screen_write_ctx *ctx, int scroll_only,
|
||||
if (wp != NULL && (wp->flags & (PANE_REDRAW|PANE_DROP)))
|
||||
goto discard;
|
||||
if (s->mode & MODE_SYNC) {
|
||||
if (ctx->scrolled != 0)
|
||||
screen_write_sync_scroll_dirty(ctx);
|
||||
if (ctx->scrolled != 0) {
|
||||
screen_write_should_draw_lines(ctx, s->rupper,
|
||||
s->rlower + 1 - s->rupper);
|
||||
}
|
||||
for (y = 0; y < screen_size_y(s); y++) {
|
||||
cl = &s->write_list[y];
|
||||
if (!TAILQ_EMPTY(&cl->items))
|
||||
@@ -2727,9 +2618,8 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
if (image_check_area(s, s->cx, s->cy, ci->used, 1) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
#ifdef ENABLE_IMAGES
|
||||
screen_write_image_damage(ctx, s->cx, s->cy, ci->used, 1);
|
||||
#endif
|
||||
|
||||
grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x,
|
||||
@@ -2859,6 +2749,11 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
|
||||
return;
|
||||
screen_write_initctx(ctx, &ttyctx, 0, 0);
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
/* Input semantics decide which image layers this text damages. */
|
||||
screen_write_image_damage(ctx, s->cx, s->cy, width, 1);
|
||||
#endif
|
||||
|
||||
/* Handle overwriting of UTF-8 characters. */
|
||||
gl = grid_get_line(s->grid, s->grid->hsize + s->cy);
|
||||
if (gl->flags & GRID_LINE_EXTENDED) {
|
||||
@@ -3086,6 +2981,9 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
|
||||
force_wide = 0;
|
||||
|
||||
/* Set the new cell. */
|
||||
#ifdef ENABLE_IMAGES
|
||||
screen_write_image_damage(ctx, cx - n, cy, n, 1);
|
||||
#endif
|
||||
grid_view_set_cell(gd, cx - n, cy, &last);
|
||||
if (force_wide)
|
||||
grid_view_set_padding(gd, cx - 1, cy, last.bg);
|
||||
@@ -3221,63 +3119,21 @@ screen_write_rawstring(struct screen_write_ctx *ctx, u_char *str, u_int len,
|
||||
tty_write(tty_cmd_rawstring, &ttyctx);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
/* Write a SIXEL image. */
|
||||
void
|
||||
screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si,
|
||||
u_int bg)
|
||||
{
|
||||
struct screen *s = ctx->s;
|
||||
struct grid *gd = s->grid;
|
||||
struct tty_ctx ttyctx;
|
||||
u_int x, y, sx, sy, cx = s->cx, cy = s->cy, i, lines;
|
||||
struct sixel_image *new;
|
||||
struct image *im;
|
||||
|
||||
if (screen_size_y(s) == 1)
|
||||
return;
|
||||
|
||||
sixel_size_in_cells(si, &x, &y);
|
||||
if (x > screen_size_x(s) || y > screen_size_y(s) - 1) {
|
||||
if (x > screen_size_x(s) - cx)
|
||||
sx = screen_size_x(s) - cx;
|
||||
else
|
||||
sx = x;
|
||||
if (y > screen_size_y(s) - 1)
|
||||
sy = screen_size_y(s) - 1;
|
||||
else
|
||||
sy = y;
|
||||
new = sixel_scale(si, 0, 0, 0, y - sy, sx, sy, 1);
|
||||
im = sixel_to_image(si);
|
||||
if (im == NULL) {
|
||||
sixel_free(si);
|
||||
if (new == NULL)
|
||||
return;
|
||||
sixel_size_in_cells(new, &x, &y);
|
||||
si = new;
|
||||
return;
|
||||
}
|
||||
|
||||
sy = screen_size_y(s) - cy;
|
||||
if (sy <= y) {
|
||||
lines = y - sy + 1;
|
||||
if (image_scroll_up(s, lines) && ctx->wp != NULL)
|
||||
ctx->wp->flags |= PANE_REDRAW;
|
||||
for (i = 0; i < lines; i++) {
|
||||
grid_view_scroll_region_up(gd, 0, screen_size_y(s) - 1,
|
||||
bg);
|
||||
screen_write_collect_scroll(ctx, bg);
|
||||
}
|
||||
ctx->scrolled += lines;
|
||||
if (lines > cy)
|
||||
screen_write_cursormove(ctx, -1, 0, 0);
|
||||
else
|
||||
screen_write_cursormove(ctx, -1, cy - lines, 0);
|
||||
}
|
||||
screen_write_collect_flush(ctx, 0, __func__);
|
||||
|
||||
screen_write_initctx(ctx, &ttyctx, 0, 0);
|
||||
ttyctx.image = image_store(s, si);
|
||||
|
||||
tty_write(tty_cmd_sixelimage, &ttyctx);
|
||||
|
||||
screen_write_cursormove(ctx, 0, cy + y, 0);
|
||||
image_write_sixel(ctx, im, bg);
|
||||
image_free(image_get_id(im));
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -93,11 +93,6 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit)
|
||||
s->tabs = NULL;
|
||||
s->sel = NULL;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
TAILQ_INIT(&s->images);
|
||||
TAILQ_INIT(&s->saved_images);
|
||||
#endif
|
||||
|
||||
s->write_list = NULL;
|
||||
s->hyperlinks = NULL;
|
||||
|
||||
@@ -131,11 +126,6 @@ screen_reinit(struct screen *s, int check)
|
||||
|
||||
screen_clear_selection(s);
|
||||
screen_free_titles(s);
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
image_free_all(s);
|
||||
#endif
|
||||
|
||||
screen_set_progress_bar(s, PROGRESS_BAR_HIDDEN, 0);
|
||||
screen_reset_hyperlinks(s);
|
||||
}
|
||||
@@ -154,10 +144,6 @@ screen_reset_hyperlinks(struct screen *s)
|
||||
void
|
||||
screen_free(struct screen *s)
|
||||
{
|
||||
#ifdef ENABLE_SIXEL
|
||||
struct image *im;
|
||||
#endif
|
||||
|
||||
free(s->sel);
|
||||
free(s->tabs);
|
||||
free(s->path);
|
||||
@@ -174,17 +160,6 @@ screen_free(struct screen *s)
|
||||
hyperlinks_free(s->hyperlinks);
|
||||
screen_free_titles(s);
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
/*
|
||||
* Images saved when entering the alternate screen stay linked in the
|
||||
* global list; move them back so they are freed and unlinked here, or
|
||||
* a later eviction would write through this freed screen.
|
||||
*/
|
||||
TAILQ_CONCAT(&s->images, &s->saved_images, entry);
|
||||
TAILQ_FOREACH(im, &s->images, entry)
|
||||
im->list = &s->images;
|
||||
image_free_all(s);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Reset tabs to default, eight spaces apart. */
|
||||
@@ -377,10 +352,6 @@ screen_resize_cursor(struct screen *s, u_int sx, u_int sy, int reflow,
|
||||
if (sy != screen_size_y(s))
|
||||
screen_resize_y(s, sy, eat_empty, &cy);
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
image_free_all(s);
|
||||
#endif
|
||||
|
||||
if (reflow)
|
||||
screen_reflow(s, sx, &cx, &cy, cursor);
|
||||
|
||||
@@ -692,10 +663,6 @@ int
|
||||
screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
|
||||
{
|
||||
u_int sx, sy;
|
||||
#ifdef ENABLE_SIXEL
|
||||
struct image *im;
|
||||
#endif
|
||||
|
||||
if (SCREEN_IS_ALTERNATE(s))
|
||||
return 0;
|
||||
sx = screen_size_x(s);
|
||||
@@ -709,12 +676,6 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
|
||||
}
|
||||
memcpy(&s->saved_cell, gc, sizeof s->saved_cell);
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
TAILQ_CONCAT(&s->saved_images, &s->images, entry);
|
||||
TAILQ_FOREACH(im, &s->saved_images, entry)
|
||||
im->list = &s->saved_images;
|
||||
#endif
|
||||
|
||||
grid_view_clear(s->grid, 0, 0, sx, sy, 8);
|
||||
|
||||
s->saved_flags = s->grid->flags;
|
||||
@@ -728,10 +689,6 @@ int
|
||||
screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
|
||||
{
|
||||
u_int sx = screen_size_x(s), sy = screen_size_y(s);
|
||||
#ifdef ENABLE_SIXEL
|
||||
struct image *im;
|
||||
#endif
|
||||
|
||||
/*
|
||||
* If the current size is different, temporarily resize to the old size
|
||||
* before copying back.
|
||||
@@ -774,13 +731,6 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
|
||||
grid_destroy(s->saved_grid);
|
||||
s->saved_grid = NULL;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
image_free_all(s);
|
||||
TAILQ_CONCAT(&s->images, &s->saved_images, entry);
|
||||
TAILQ_FOREACH(im, &s->images, entry)
|
||||
im->list = &s->images;
|
||||
#endif
|
||||
|
||||
if (s->cx > screen_size_x(s) - 1)
|
||||
s->cx = screen_size_x(s) - 1;
|
||||
if (s->cy > screen_size_y(s) - 1)
|
||||
|
||||
+81
-17
@@ -199,6 +199,7 @@ server_client_create(int fd)
|
||||
c->click_wp = -1;
|
||||
|
||||
TAILQ_INIT(&c->input_requests);
|
||||
TAILQ_INIT(&c->pending_damage);
|
||||
|
||||
TAILQ_INSERT_TAIL(&clients, c, entry);
|
||||
log_debug("new client %p", c);
|
||||
@@ -358,10 +359,30 @@ server_client_set_session(struct client *c, struct session *s)
|
||||
server_client_fire_session_changed(c, old);
|
||||
|
||||
/*
|
||||
* Redraw if the session or displayed window changed. Use the
|
||||
* cached scene because the session's current window is already set.
|
||||
* A full redraw is only needed if the client's session or
|
||||
* current window actually changed - not if this merely
|
||||
* confirmed the client is still looking at the same window
|
||||
* (as happens when switch-client -t targets a pane in the
|
||||
* already-current window, e.g. clicking a pane name in a
|
||||
* second #{P:} status line: the default MouseDown1Status
|
||||
* binding resolves that click to switch-client -t=, which
|
||||
* reaches here regardless of whether anything besides the
|
||||
* active pane changed). Redrawing unconditionally here
|
||||
* forced a full window redraw for what should have been
|
||||
* just an active-pane change, already handled narrowly by
|
||||
* window_set_active_pane() and window_redraw_active_switch()
|
||||
* before this is reached.
|
||||
*
|
||||
* old and s may be the same session object, whose curw was
|
||||
* already updated to the new window before this function was
|
||||
* called - old->curw and s->curw would then read the same,
|
||||
* already-current value, so comparing them can never detect
|
||||
* a same-session window change. Compare against the client's
|
||||
* own cached scene instead, which only reflects what it has
|
||||
* actually drawn.
|
||||
*/
|
||||
if (old != s || !redraw_client_has_window(c, s->curw->window))
|
||||
if (old == NULL || old != s ||
|
||||
!redraw_client_has_window(c, s->curw->window))
|
||||
server_redraw_client(c);
|
||||
}
|
||||
|
||||
@@ -413,6 +434,7 @@ server_client_lost(struct client *c)
|
||||
|
||||
status_free(c);
|
||||
input_cancel_requests(c);
|
||||
redraw_free_pending_damage(c);
|
||||
|
||||
free(c->title);
|
||||
free(c->path);
|
||||
@@ -1355,8 +1377,18 @@ server_client_key_callback(struct cmdq_item *item, void *data)
|
||||
m->key = key;
|
||||
|
||||
/*
|
||||
* Synchronize direct drag output with the later damage redraw
|
||||
* before invoking the drag callback.
|
||||
* Mouse drag is in progress, so fire the callback (now that
|
||||
* the mouse event is valid).
|
||||
*
|
||||
* Start a synchronized-output region here rather than
|
||||
* leaving it to whatever redraw eventually follows: a drag
|
||||
* callback may write directly via the pane's fast path
|
||||
* immediately, with any correction only arriving later via
|
||||
* redraw_client_damage(), which opens its own sync region.
|
||||
* Since tty_sync_end() is only called once, at the very end
|
||||
* of this client's pass in server_client_reset_state(),
|
||||
* starting it here merges both into one atomic terminal
|
||||
* update instead of two visible frames.
|
||||
*/
|
||||
if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) {
|
||||
tty_sync_start(&c->tty);
|
||||
@@ -1786,8 +1818,15 @@ server_client_loop(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* Clear window redraw state after processing all clients. Deferred
|
||||
* redraws are preserved in client flags.
|
||||
* Any windows will have been redrawn as part of clients, so clear
|
||||
* their flags now. A client whose redraw was deferred this pass
|
||||
* (waiting for outstanding tty output to drain) has already copied
|
||||
* window damage onto its own pending_damage in
|
||||
* server_client_check_redraw() (see redraw_defer_damage()) to
|
||||
* compose precisely on a later pass, or escalated to
|
||||
* CLIENT_REDRAWWINDOW/CLIENT_REDRAWSCROLLBARS for whole-pane needs
|
||||
* that aren't rectangle-shaped, so PANE_REDRAW/PANE_REDRAWSCROLLBAR
|
||||
* and window damage can simply be cleared unconditionally here.
|
||||
*/
|
||||
RB_FOREACH(w, windows, &windows) {
|
||||
TAILQ_FOREACH(wp, &w->panes, entry) {
|
||||
@@ -2347,6 +2386,8 @@ server_client_any_pane_redraw(struct client *c)
|
||||
return (1);
|
||||
if (!TAILQ_EMPTY(&w->damage))
|
||||
return (1);
|
||||
if (!TAILQ_EMPTY(&c->pending_damage))
|
||||
return (1);
|
||||
TAILQ_FOREACH(wp, &w->panes, entry) {
|
||||
if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR))
|
||||
return (1);
|
||||
@@ -2363,7 +2404,6 @@ server_client_check_redraw(struct client *c)
|
||||
struct window *w = s->curw->window;
|
||||
struct window_pane *wp;
|
||||
int needed, tflags, mode = tty->mode;
|
||||
int damaged = !TAILQ_EMPTY(&w->damage);
|
||||
struct timeval tv = { .tv_usec = 1000 };
|
||||
static struct event ev;
|
||||
size_t n;
|
||||
@@ -2389,12 +2429,29 @@ server_client_check_redraw(struct client *c)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Ignore output queued within the current synchronized frame. */
|
||||
/*
|
||||
* If there is outstanding data, defer the redraw until it has been
|
||||
* consumed. We can just add a timer to get out of the event loop and
|
||||
* end up back here. server_client_loop() clears PANE_REDRAW,
|
||||
* PANE_REDRAWSCROLLBAR and window damage unconditionally every pass,
|
||||
* so window damage is copied onto this client's own pending_damage
|
||||
* (redraw_defer_damage()) to survive that and still be composed
|
||||
* precisely on a later pass; a whole-pane need (PANE_REDRAW) or a
|
||||
* pane's scrollbar isn't rectangle-shaped the same way, so those
|
||||
* still escalate to a coarser, persistent client flag as before.
|
||||
*
|
||||
* If a synchronized-output frame is open, discount anything queued
|
||||
* since it started (down to sync_offset, the length when it opened):
|
||||
* those bytes are already part of the frame this pass is committed
|
||||
* to flushing (see tty_sync_start()), not a reason to defer this
|
||||
* pass's redraw - without this, a mouse-drag callback that itself
|
||||
* opens the frame before writing anything would see its own
|
||||
* just-queued bytes as "outstanding output" and defer against
|
||||
* itself every single motion event.
|
||||
*/
|
||||
n = EVBUFFER_LENGTH(tty->out);
|
||||
if ((tty->flags & TTY_SYNCING) && n > tty->sync_offset)
|
||||
n = tty->sync_offset;
|
||||
|
||||
/* Defer until output drains, preserving damage in client flags. */
|
||||
if (n != 0 || (tty->flags & TTY_BLOCK)) {
|
||||
if (n != 0)
|
||||
log_debug("%s: redraw deferred (%zu left)", c->name, n);
|
||||
@@ -2406,10 +2463,8 @@ server_client_check_redraw(struct client *c)
|
||||
log_debug("redraw timer started");
|
||||
evtimer_add(&ev, &tv);
|
||||
}
|
||||
if (damaged) {
|
||||
c->flags |= CLIENT_REDRAWWINDOW;
|
||||
return;
|
||||
}
|
||||
if (!TAILQ_EMPTY(&w->damage))
|
||||
redraw_defer_damage(c);
|
||||
TAILQ_FOREACH(wp, &w->panes, entry) {
|
||||
if (wp->flags & PANE_REDRAW) {
|
||||
c->flags |= CLIENT_REDRAWWINDOW;
|
||||
@@ -2444,8 +2499,17 @@ server_client_check_redraw(struct client *c)
|
||||
}
|
||||
}
|
||||
|
||||
/* Draw damage here if no client redraw flags will handle it. */
|
||||
if (damaged && (c->flags & CLIENT_ALLREDRAWFLAGS) == 0)
|
||||
/*
|
||||
* Window damage (and any damage this client missed on a
|
||||
* previous deferred pass - see redraw_defer_damage()) is
|
||||
* also what makes server_client_any_pane_redraw() decide a
|
||||
* redraw is needed at all, independently of any
|
||||
* CLIENT_ALLREDRAWFLAGS bit. Consume both here, since
|
||||
* server_client_loop() clears window damage unconditionally
|
||||
* every pass regardless of whether it was actually drawn.
|
||||
*/
|
||||
if ((!TAILQ_EMPTY(&w->damage) || !TAILQ_EMPTY(&c->pending_damage)) &&
|
||||
!(c->flags & CLIENT_ALLREDRAWFLAGS))
|
||||
redraw_client_damage(c);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
|
||||
/* Default style. */
|
||||
static struct style style_default = {
|
||||
{ { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 },
|
||||
{ { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0
|
||||
},
|
||||
0,
|
||||
0,
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.\" $OpenBSD: tmux.1,v 1.1173 2026/09/23 12:37:50 nicm Exp $
|
||||
.\" $OpenBSD: tmux.1,v 1.1172 2026/09/22 06:58:05 nicm Exp $
|
||||
.\"
|
||||
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||
.\"
|
||||
@@ -14,7 +14,7 @@
|
||||
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
.\"
|
||||
.Dd $Mdocdate: September 23 2026 $
|
||||
.Dd $Mdocdate: September 22 2026 $
|
||||
.Dt TMUX 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
@@ -994,19 +994,13 @@ Will run:
|
||||
/bin/sh \-c \[aq]vi \[ti]/.tmux.conf\[aq]
|
||||
.Ed
|
||||
.Pp
|
||||
Unless specified, tmux uses the value of
|
||||
.Ic default\-shell
|
||||
in place of
|
||||
.Pa /bin/sh .
|
||||
.Pp
|
||||
Additionally, the
|
||||
.Ic new\-window ,
|
||||
.Ic new\-session ,
|
||||
.Ic split\-window ,
|
||||
.Ic respawn\-window ,
|
||||
.Ic respawn\-pane
|
||||
.Ic respawn\-window
|
||||
and
|
||||
.Ic display\-popup
|
||||
.Ic respawn\-pane
|
||||
commands allow
|
||||
.Ar shell\-command
|
||||
to be given as multiple arguments and executed directly (without
|
||||
@@ -5106,12 +5100,20 @@ Supports extended keys.
|
||||
Supports focus reporting.
|
||||
.It hyperlinks
|
||||
Supports OSC 8 hyperlinks.
|
||||
.It image-quadrants
|
||||
Allows image fallbacks to use Unicode quadrant block characters.
|
||||
.It image-sextants
|
||||
Allows image fallbacks to use Unicode sextant block characters.
|
||||
.It ignorefkeys
|
||||
Ignore function keys from
|
||||
.Xr terminfo 5
|
||||
and use the
|
||||
.Nm
|
||||
internal set only.
|
||||
.It kitty
|
||||
Supports Kitty graphics using Unicode placeholders.
|
||||
The terminal must support the Kitty Unicode placeholder extension, not only
|
||||
the basic Kitty graphics protocol.
|
||||
.It margins
|
||||
Supports DECSLRM margins.
|
||||
.It mouse
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* $OpenBSD: tmux.h,v 1.1448 2026/09/24 11:19:39 nicm Exp $ */
|
||||
/* $OpenBSD: tmux.h,v 1.1447 2026/09/22 14:10:26 nicm Exp $ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||
@@ -77,7 +77,12 @@ struct screen_write_cline;
|
||||
struct screen_write_ctx;
|
||||
struct session;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
struct image;
|
||||
struct image_backend;
|
||||
struct image_rect;
|
||||
struct image_line;
|
||||
struct image_store;
|
||||
struct sixel_image;
|
||||
#endif
|
||||
|
||||
@@ -863,7 +868,7 @@ struct colour_palette {
|
||||
struct grid_cell {
|
||||
struct utf8_data data;
|
||||
u_short attr;
|
||||
u_char flags;
|
||||
u_short flags;
|
||||
int fg;
|
||||
int bg;
|
||||
int us;
|
||||
@@ -874,7 +879,7 @@ struct grid_cell {
|
||||
struct grid_extd_entry {
|
||||
utf8_char data;
|
||||
u_short attr;
|
||||
u_char flags;
|
||||
u_short flags;
|
||||
int fg;
|
||||
int bg;
|
||||
int us;
|
||||
@@ -892,7 +897,7 @@ struct grid_cell_entry {
|
||||
u_char data;
|
||||
} data;
|
||||
};
|
||||
u_char flags;
|
||||
u_short flags;
|
||||
} __packed;
|
||||
|
||||
/* OSC 133 data for a grid line. */
|
||||
@@ -916,6 +921,9 @@ struct grid_line {
|
||||
u_int time;
|
||||
struct osc133_data osc133_data;
|
||||
u_short flags;
|
||||
#ifdef ENABLE_IMAGES
|
||||
struct image_line *images;
|
||||
#endif
|
||||
};
|
||||
|
||||
/* Entire grid of cells. */
|
||||
@@ -935,6 +943,9 @@ struct grid {
|
||||
u_int scroll_generation;
|
||||
|
||||
struct grid_line *linedata;
|
||||
#ifdef ENABLE_IMAGES
|
||||
struct image_store *images;
|
||||
#endif
|
||||
};
|
||||
|
||||
/* Virtual cursor in a grid. */
|
||||
@@ -1026,24 +1037,65 @@ struct style {
|
||||
u_int link;
|
||||
};
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
/* Image. */
|
||||
struct image {
|
||||
struct screen *s;
|
||||
struct sixel_image *data;
|
||||
char *fallback;
|
||||
#ifdef ENABLE_IMAGES
|
||||
/* Private ACS keys used by the text image backend. */
|
||||
#define TTY_ACS_IMAGE_SHADE_LIGHT 0x80
|
||||
#define TTY_ACS_IMAGE_SHADE_MEDIUM 0x81
|
||||
#define TTY_ACS_IMAGE_SHADE_DARK 0x82
|
||||
#define TTY_ACS_IMAGE_BLOCK 0x83
|
||||
#define TTY_ACS_IMAGE_HALF_UPPER 0x84
|
||||
#define TTY_ACS_IMAGE_HALF_LOWER 0x85
|
||||
#define TTY_ACS_IMAGE_HALF_LEFT 0x86
|
||||
#define TTY_ACS_IMAGE_HALF_RIGHT 0x87
|
||||
#define TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT 0x88
|
||||
#define TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT 0x89
|
||||
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT 0x8a
|
||||
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT 0x8b
|
||||
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT 0x8c
|
||||
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT 0x8d
|
||||
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT 0x8e
|
||||
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT 0x8f
|
||||
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT 0x90
|
||||
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT 0x91
|
||||
#define TTY_ACS_IMAGE_SEXTANT_FIRST 0x92
|
||||
#define TTY_ACS_IMAGE_SEXTANT_LAST 0xcd
|
||||
|
||||
u_int px;
|
||||
u_int py;
|
||||
struct image_sample {
|
||||
u_char red;
|
||||
u_char green;
|
||||
u_char blue;
|
||||
u_char alpha;
|
||||
u_char brightness;
|
||||
};
|
||||
#define IMAGE_SAMPLE_COLUMNS 2
|
||||
#define IMAGE_SAMPLE_ROWS 6
|
||||
struct image_cell {
|
||||
struct image_sample whole;
|
||||
struct image_sample samples[IMAGE_SAMPLE_ROWS][IMAGE_SAMPLE_COLUMNS];
|
||||
};
|
||||
struct image {
|
||||
u_int id;
|
||||
u_int references;
|
||||
u_int flags;
|
||||
u_int parent_id;
|
||||
u_int source_id;
|
||||
u_int width;
|
||||
u_int height;
|
||||
u_int canvas_width;
|
||||
u_int canvas_height;
|
||||
u_int sx;
|
||||
u_int sy;
|
||||
size_t stride;
|
||||
size_t size;
|
||||
u_char *pixels;
|
||||
struct sixel_image *sixel;
|
||||
struct image_cell *cells;
|
||||
void *fallback_data;
|
||||
|
||||
struct images *list;
|
||||
TAILQ_ENTRY (image) entry;
|
||||
|
||||
TAILQ_ENTRY (image) all_entry;
|
||||
RB_ENTRY(image) entry;
|
||||
};
|
||||
TAILQ_HEAD(images, image);
|
||||
RB_HEAD(images, image);
|
||||
#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024)
|
||||
#endif
|
||||
|
||||
/* Cursor style. */
|
||||
@@ -1102,11 +1154,6 @@ struct screen {
|
||||
bitstr_t *tabs;
|
||||
struct screen_sel *sel;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
struct images images;
|
||||
struct images saved_images;
|
||||
#endif
|
||||
|
||||
struct screen_write_cline *write_list;
|
||||
|
||||
struct hyperlinks *hyperlinks;
|
||||
@@ -1124,6 +1171,7 @@ struct screen_write_ctx {
|
||||
#define SCREEN_WRITE_SYNC 0x1
|
||||
#define SCREEN_WRITE_OBSCURED 0x2
|
||||
#define SCREEN_WRITE_CHECKED_IF_OBSCURED 0x4
|
||||
#define SCREEN_WRITE_INPUT 0x8
|
||||
|
||||
screen_write_init_ctx_cb init_ctx_cb;
|
||||
void *arg;
|
||||
@@ -1348,10 +1396,6 @@ struct window_pane {
|
||||
|
||||
bitstr_t *sync_dirty;
|
||||
u_int sync_dirty_size;
|
||||
u_int sync_scrolled;
|
||||
u_int sync_rupper;
|
||||
u_int sync_rlower;
|
||||
u_int sync_bg;
|
||||
|
||||
u_int sb_slider_y;
|
||||
u_int sb_slider_h;
|
||||
@@ -1407,7 +1451,7 @@ struct window_pane {
|
||||
struct screen base;
|
||||
|
||||
struct screen status_screen;
|
||||
u_int status_generation;
|
||||
u_int status_serial;
|
||||
|
||||
TAILQ_HEAD(, window_mode_entry) modes;
|
||||
|
||||
@@ -1484,6 +1528,10 @@ struct window {
|
||||
|
||||
struct redraw_damages damage;
|
||||
u_int damage_count;
|
||||
#ifdef ENABLE_IMAGES
|
||||
uint64_t image_scroll_epoch;
|
||||
int image_scroll_pending;
|
||||
#endif
|
||||
|
||||
struct menu_data *menu;
|
||||
u_int menu_last_px;
|
||||
@@ -1752,7 +1800,13 @@ struct tty_term {
|
||||
#define TERM_VT100LIKE 0x20
|
||||
#define TERM_SIXEL 0x40
|
||||
#define TERM_INVALIDMS 0x80
|
||||
#define TERM_NOREPLACE 0x100
|
||||
#define TERM_KITTY 0x100
|
||||
#ifdef ENABLE_IMAGES
|
||||
#define TERM_IMAGE_QUADRANTS 0x200
|
||||
#define TERM_IMAGE_SEXTANTS 0x400
|
||||
#define TERM_IMAGESCROLL 0x1000
|
||||
#endif
|
||||
#define TERM_NOREPLACE 0x800
|
||||
int flags;
|
||||
|
||||
LIST_ENTRY(tty_term) entry;
|
||||
@@ -1810,6 +1864,12 @@ struct tty {
|
||||
struct event timer;
|
||||
size_t discarded;
|
||||
|
||||
/*
|
||||
* Buffer length at the instant a synchronized-output frame opened
|
||||
* (tty_sync_start()), so server_client_check_redraw()'s "is there
|
||||
* already outstanding output" check can discount whatever this
|
||||
* pass itself queued into that frame - see tty_sync_start().
|
||||
*/
|
||||
size_t sync_offset;
|
||||
|
||||
struct termios tio;
|
||||
@@ -1856,6 +1916,13 @@ struct tty {
|
||||
|
||||
struct event key_timer;
|
||||
struct tty_key *key_tree;
|
||||
#ifdef ENABLE_IMAGES
|
||||
const struct image_backend *image_backend;
|
||||
void *image_data;
|
||||
struct window *image_scroll_window;
|
||||
uint64_t image_scroll_epoch;
|
||||
int image_scroll_failed;
|
||||
#endif
|
||||
};
|
||||
|
||||
/* Terminal command context. */
|
||||
@@ -1891,7 +1958,7 @@ struct tty_ctx {
|
||||
size_t size;
|
||||
} sel;
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
struct image *image;
|
||||
#endif
|
||||
};
|
||||
@@ -2252,6 +2319,23 @@ struct client {
|
||||
|
||||
struct redraw_scene *redraw_scene;
|
||||
|
||||
/*
|
||||
* Damage this client missed because its redraw was deferred (pending
|
||||
* tty output) on the pass it was reported - server_client_loop()
|
||||
* frees w->damage unconditionally every pass regardless of whether
|
||||
* every attached client got to consume it, so a deferred client's
|
||||
* copy is kept here to compose precisely on a later pass instead of
|
||||
* escalating to a full-window redraw. See redraw_defer_damage() and
|
||||
* redraw_client_damage() (screen-redraw.c). pending_damage_id is the
|
||||
* id of the window these rectangles were copied for (window ids are
|
||||
* unique and never reused), used to discard them if the client's
|
||||
* current window has since changed instead of composing them
|
||||
* against the wrong window's scene.
|
||||
*/
|
||||
struct redraw_damages pending_damage;
|
||||
u_int pending_damage_count;
|
||||
u_int pending_damage_id;
|
||||
|
||||
struct event repeat_timer;
|
||||
|
||||
struct event click_timer;
|
||||
@@ -2990,10 +3074,6 @@ void tty_cmd_scrolldown(struct tty *, const struct tty_ctx *);
|
||||
void tty_cmd_reverseindex(struct tty *, const struct tty_ctx *);
|
||||
void tty_cmd_setselection(struct tty *, const struct tty_ctx *);
|
||||
void tty_cmd_rawstring(struct tty *, const struct tty_ctx *);
|
||||
#ifdef ENABLE_SIXEL
|
||||
void tty_cmd_sixelimage(struct tty *, const struct tty_ctx *);
|
||||
void tty_draw_images(struct client *, struct window_pane *);
|
||||
#endif
|
||||
void tty_cmd_syncstart(struct tty *, const struct tty_ctx *);
|
||||
void tty_default_colours(struct grid_cell *, struct window_pane *, u_int *);
|
||||
|
||||
@@ -3035,6 +3115,7 @@ void tty_default_features(struct client *, const char *, u_int);
|
||||
/* tty-acs.c */
|
||||
int tty_acs_needed(struct tty *);
|
||||
const char *tty_acs_get(struct tty *, u_char);
|
||||
u_char tty_acs_image_sextant(u_int);
|
||||
int tty_acs_reverse_get(struct tty *, const char *, size_t);
|
||||
const struct utf8_data *tty_acs_double_borders(int);
|
||||
const struct utf8_data *tty_acs_heavy_borders(int);
|
||||
@@ -3598,7 +3679,7 @@ void screen_write_mode_clear(struct screen_write_ctx *, int);
|
||||
void screen_write_start_sync(struct window_pane *);
|
||||
void screen_write_stop_sync(struct window_pane *);
|
||||
void screen_write_end_sync(struct screen_write_ctx *);
|
||||
void screen_write_sync_clear_dirty(struct window_pane *);
|
||||
void screen_write_clear_dirty(struct window_pane *);
|
||||
void screen_write_cursorup(struct screen_write_ctx *, u_int);
|
||||
void screen_write_cursordown(struct screen_write_ctx *, u_int);
|
||||
void screen_write_cursorright(struct screen_write_ctx *, u_int);
|
||||
@@ -3632,7 +3713,7 @@ void screen_write_setselection(struct screen_write_ctx *, const char *,
|
||||
u_char *, u_int);
|
||||
void screen_write_rawstring(struct screen_write_ctx *, u_char *, u_int,
|
||||
int);
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
void screen_write_sixelimage(struct screen_write_ctx *,
|
||||
struct sixel_image *, u_int);
|
||||
#endif
|
||||
@@ -3650,8 +3731,15 @@ int redraw_client_has_window(struct client *, struct window *);
|
||||
void redraw_invalidate_scene(struct window *);
|
||||
void redraw_invalidate_all_scenes(void);
|
||||
void redraw_damage_window(struct window *, u_int, u_int, u_int, u_int);
|
||||
#ifdef ENABLE_IMAGES
|
||||
void redraw_damage_window_scroll(struct window *, u_int, u_int, u_int,
|
||||
u_int);
|
||||
void redraw_image_scroll_result(struct tty *, const struct tty_ctx *, int);
|
||||
#endif
|
||||
void redraw_free_damage(struct window *);
|
||||
void redraw_client_damage(struct client *);
|
||||
void redraw_defer_damage(struct client *);
|
||||
void redraw_free_pending_damage(struct client *);
|
||||
int redraw_get_status_border_cell_type(struct redraw_span **, u_int);
|
||||
|
||||
/* screen.c */
|
||||
@@ -3825,9 +3913,9 @@ int window_pane_get_pane_status(struct window_pane *);
|
||||
struct style_range *window_pane_status_get_range(struct window_pane *, u_int,
|
||||
u_int);
|
||||
int window_pane_is_floating(struct window_pane *);
|
||||
void window_pane_redraw_floating(struct window *,
|
||||
struct window_pane *, int, int, int, int);
|
||||
int window_pane_is_floating_with_hidden(struct window_pane *);
|
||||
void window_redraw_floating_pane(struct window_pane *, int, int,
|
||||
int, int);
|
||||
|
||||
/* window-border.c */
|
||||
void window_set_fill_cells(struct window *);
|
||||
@@ -4216,17 +4304,115 @@ void spawn_editor_finish(struct window_pane *);
|
||||
/* regsub.c */
|
||||
char *regsub(const char *, const char *, const char *, int);
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
#ifdef ENABLE_IMAGES
|
||||
/* image.c */
|
||||
int image_free_all(struct screen *);
|
||||
struct image *image_store(struct screen *, struct sixel_image *);
|
||||
int image_check_line(struct screen *, u_int, u_int);
|
||||
int image_check_area(struct screen *, u_int, u_int, u_int, u_int);
|
||||
int image_scroll_up(struct screen *, u_int);
|
||||
#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 *);
|
||||
struct image *image_create_view(struct image *, u_int, u_int, u_int,
|
||||
u_int, u_int, u_int, u_int, u_int, u_int, u_int);
|
||||
struct image *image_find(u_int);
|
||||
u_int image_get_id(const struct image *);
|
||||
void image_get_size(const struct image *, u_int *, u_int *);
|
||||
void image_get_canvas_size(const struct image *, u_int *, u_int *);
|
||||
void image_get_size_in_cells(const struct image *, u_int *, u_int *);
|
||||
const u_char *image_get_pixels(const struct image *, size_t *, size_t *);
|
||||
void image_set_no_cursor(struct image *);
|
||||
struct sixel_image *image_get_sixel(const struct image *);
|
||||
void image_set_sixel(struct image *, struct sixel_image *);
|
||||
void image_ref(u_int);
|
||||
void image_free(u_int);
|
||||
void image_write_sixel(struct screen_write_ctx *, struct image *,
|
||||
u_int);
|
||||
void image_write_kitty(struct screen_write_ctx *, struct image *,
|
||||
u_int, u_int, u_int, int32_t);
|
||||
void image_get_pixel_rect(const struct image *, u_int, u_int,
|
||||
u_int, u_int, u_int *, u_int *, u_int *, u_int *);
|
||||
void image_size_in_cells(u_int, u_int, u_int, u_int, u_int *,
|
||||
u_int *);
|
||||
u_char *image_base64_decode(const char *, size_t, size_t, size_t *);
|
||||
u_char *image_png_decode(const u_char *, size_t, size_t, u_int *,
|
||||
u_int *);
|
||||
void image_redraw_area(struct screen_write_ctx *, u_int, u_int,
|
||||
u_int, u_int);
|
||||
void image_redraw_all(struct screen_write_ctx *);
|
||||
void image_redraw_scroll(struct screen_write_ctx *, u_int);
|
||||
void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
|
||||
void image_redraw_finish(struct tty *);
|
||||
void image_draw_flush(struct tty *);
|
||||
int image_backend_flags(struct tty *);
|
||||
int image_tty_update(struct tty *);
|
||||
void image_tty_geometry_changed(struct tty *);
|
||||
void image_tty_free(struct tty *, int);
|
||||
void image_draw_line(struct tty *, struct screen *, u_int, u_int,
|
||||
u_int, u_int, u_int, int, const struct tty_style_ctx *);
|
||||
void image_get_fallback_cell(struct tty *, struct image *, u_int,
|
||||
u_int, const struct grid_cell *, struct grid_cell *,
|
||||
const struct tty_style_ctx *);
|
||||
const struct image_cell *image_get_cell(struct image *, u_int, u_int);
|
||||
void image_free_fallback(struct image *);
|
||||
int image_get_fallback_at(struct tty *, struct screen *, u_int,
|
||||
u_int, const struct grid_cell *, struct grid_cell *,
|
||||
const struct tty_style_ctx *);
|
||||
struct image *image_rect_get_image(const struct image_rect *);
|
||||
const struct grid_cell *image_rect_get_cell(
|
||||
const struct image_rect *);
|
||||
void image_rect_get_coords(const struct image_rect *,
|
||||
u_int *, u_int *, u_int *, u_int *, u_int *, u_int *);
|
||||
int32_t image_rect_get_z(const struct image_rect *);
|
||||
void image_clear(struct screen_write_ctx *, u_int);
|
||||
void image_clear_kitty(struct screen_write_ctx *, char, u_int,
|
||||
u_int, int32_t);
|
||||
void image_grid_damage(struct grid *, u_int, u_int, u_int, u_int);
|
||||
void image_grid_free_line(struct grid *, struct grid_line *);
|
||||
void image_grid_free(struct grid *);
|
||||
void image_grid_move_cells(struct grid *, u_int, u_int, u_int,
|
||||
u_int);
|
||||
void image_grid_duplicate_lines(struct grid *, u_int, struct grid *,
|
||||
u_int, u_int);
|
||||
void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *,
|
||||
u_int, u_int, u_int, u_int);
|
||||
void image_grid_resize_width(struct grid *, u_int);
|
||||
int image_grid_line_has_images(const struct grid_line *);
|
||||
int image_grid_check_area(struct grid *, u_int, u_int, u_int,
|
||||
u_int);
|
||||
int image_grid_next_span(struct grid *, u_int, u_int, u_int,
|
||||
u_int *, u_int *);
|
||||
int image_grid_get_source(struct grid *, u_int, u_int,
|
||||
struct image *, u_int *, u_int *);
|
||||
void image_place_cell_kitty(struct screen_write_ctx *, struct image *,
|
||||
u_int, u_int, u_int, u_int, u_int, u_int, int32_t);
|
||||
#define KITTY_PARSE_ERROR -1
|
||||
#define KITTY_PARSE_OK 0
|
||||
#define KITTY_PARSE_MORE 1
|
||||
#define KITTY_PARSE_MISSING 2
|
||||
struct image *kitty_parse_image(void **, const u_char *, size_t, u_int,
|
||||
u_int, u_int *, u_int *, u_int *, char *, char *, u_int *,
|
||||
int32_t *, int *);
|
||||
int kitty_placeholder_to_image(void *, struct grid *,
|
||||
struct grid_cell *, u_int, u_int, struct image **, u_int *,
|
||||
u_int *, u_int *, u_int *, int32_t *);
|
||||
void kitty_free_state(void *);
|
||||
void kitty_draw_rect(struct tty *,
|
||||
const struct image_rect *, const struct tty_style_ctx *);
|
||||
void kitty_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
|
||||
void kitty_redraw_finish(struct tty *);
|
||||
void kitty_free_output_state(struct tty *, int);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
/* image-sixel.c */
|
||||
#define SIXEL_COLOUR_REGISTERS 1024
|
||||
struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int);
|
||||
void sixel_draw_rect(struct tty *,
|
||||
const struct image_rect *, const struct tty_style_ctx *);
|
||||
void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
|
||||
void sixel_free_output(struct tty *, int);
|
||||
void sixel_flush_output(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 *);
|
||||
void sixel_log(struct sixel_image *);
|
||||
void sixel_size_in_cells(struct sixel_image *, u_int *, u_int *);
|
||||
@@ -4235,6 +4421,7 @@ struct sixel_image *sixel_scale(struct sixel_image *, u_int, u_int, u_int,
|
||||
char *sixel_print(struct sixel_image *, struct sixel_image *,
|
||||
size_t *);
|
||||
struct screen *sixel_to_screen(struct sixel_image *);
|
||||
struct image *sixel_to_image(struct sixel_image *);
|
||||
#endif
|
||||
|
||||
/* server-acl.c */
|
||||
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Generate the Unicode diacritic table used by Kitty image placeholders.
|
||||
#
|
||||
# Input must be UnicodeData.txt from Unicode 6.0.0:
|
||||
# https://www.unicode.org/Public/6.0.0/ucd/UnicodeData.txt
|
||||
# SHA-256: 90b45b777346bef027556f9c6cb3ea5d7d745bd60d6762855c08d8a22d34f771
|
||||
#
|
||||
# The selection follows the public Kitty graphics protocol: nonspacing marks
|
||||
# with canonical combining class 230, NSM bidi class, no decomposition, minus
|
||||
# characters which may be combined or normalized in ways unsuitable for a
|
||||
# row or column index.
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: $0 UnicodeData.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
awk -F ';' '
|
||||
BEGIN {
|
||||
omit["0300"] = omit["0301"] = omit["0302"] = 1
|
||||
omit["0303"] = omit["0304"] = omit["0306"] = 1
|
||||
omit["0307"] = omit["0308"] = omit["0309"] = 1
|
||||
omit["030A"] = omit["030B"] = omit["030C"] = 1
|
||||
omit["030F"] = omit["0311"] = omit["0313"] = 1
|
||||
omit["0314"] = omit["0342"] = omit["0653"] = 1
|
||||
omit["0654"] = 1
|
||||
}
|
||||
$3 == "Mn" && $4 == "230" && $5 == "NSM" && $6 == "" && !omit[$1] {
|
||||
point[++count] = $1
|
||||
}
|
||||
END {
|
||||
if (count != 297) {
|
||||
printf "expected 297 diacritics, found %u\n", count > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
print "static const uint32_t kitty_diacritics[] = {"
|
||||
for (i = 1; i <= count; i++) {
|
||||
if ((i - 1) % 8 == 0)
|
||||
printf "\t"
|
||||
printf "0x%s", point[i]
|
||||
if (i != count)
|
||||
printf ","
|
||||
if (i == count || i % 8 == 0)
|
||||
printf "\n"
|
||||
else
|
||||
printf " "
|
||||
}
|
||||
print "};"
|
||||
}
|
||||
' "$1"
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Compact visual check for image subcell geometry and layering. Run inside a
|
||||
# tmux pane on Kitty or a SIXEL terminal; it needs at least 90 columns/24 rows.
|
||||
|
||||
if [ -z "$TMUX" ]; then
|
||||
echo "Run this script inside tmux." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
columns=$(tput cols 2>/dev/null || echo 0)
|
||||
lines=$(tput lines 2>/dev/null || echo 0)
|
||||
if [ "$columns" -lt 90 ] || [ "$lines" -lt 24 ]; then
|
||||
echo "This test needs at least 90 columns and 24 rows." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
at()
|
||||
{
|
||||
printf '\033[%s;%sH' "$1" "$2"
|
||||
}
|
||||
|
||||
kitty()
|
||||
{
|
||||
printf '\033_G%s\033\\' "$1"
|
||||
}
|
||||
|
||||
kitty_solid()
|
||||
{
|
||||
id=$1
|
||||
rgba=$2
|
||||
payload=$(i=0; while [ "$i" -lt 256 ]; do
|
||||
printf "$rgba"
|
||||
i=$((i + 1))
|
||||
done | base64 | tr -d '\n')
|
||||
kitty "a=t,q=2,f=32,s=16,v=16,i=$id;$payload"
|
||||
}
|
||||
|
||||
sixel_folder()
|
||||
{
|
||||
printf '\033P0;1q"1;1;26;26#0;2;50;50;50#1;2;0;0;0#2;2;49;49;49#3;2;33;33;33#4;2;47;47;47#5;2;44;44;44#6;2;48;48;48#2?O?C!5?S!4?__?_?__$#0?_wW!5KGwo_-#0?~~!8?!10@BB}w$#2!12?a_A?A?A?A??@E-#0?~~!7?KK~~KK!7?~~$#2!9?K!6?K-#0?F^[owowowowowowowowooS^F$#2?G!10?@@!7?G??G-\033\\'
|
||||
}
|
||||
|
||||
feature_info=$(tmux display-message -p \
|
||||
'term=#{client_termname} features=#{client_termfeatures} cells=#{client_cell_width}x#{client_cell_height}')
|
||||
|
||||
printf '\033[2J\033[H\033[?25l'
|
||||
at 1 2
|
||||
printf 'tmux image edge-case check'
|
||||
at 2 2
|
||||
printf '%s' "$feature_info"
|
||||
|
||||
kitty_solid 201 '\377\000\000\377'
|
||||
kitty_solid 202 '\000\377\000\377'
|
||||
kitty_solid 203 '\000\000\377\377'
|
||||
|
||||
# The 26-pixel folder occupies whole grid cells, but its visible right and
|
||||
# bottom edges must remain partial rather than stretching to those boundaries.
|
||||
at 4 2
|
||||
printf '1. partial SIXEL: folder should be the same size as direct output'
|
||||
at 4 72
|
||||
sixel_folder
|
||||
|
||||
# Chawan emits X/Y for its image padding. The red square must begin slightly
|
||||
# below and right of the [img] anchor, never over it.
|
||||
at 8 2
|
||||
printf '2. Kitty X/Y: RED begins below/right of the [img] anchor'
|
||||
at 8 72
|
||||
printf '[img]'
|
||||
at 9 72
|
||||
kitty 'a=p,q=2,C=1,i=201,p=1,x=0,y=0,w=16,h=16,c=3,r=2,X=6,Y=6'
|
||||
|
||||
# Positive Kitty z overlays text and other lower z images; negative z is
|
||||
# below text. The shared red/blue edge makes any wrong order obvious.
|
||||
at 12 2
|
||||
printf '3. Kitty z: BLUE covers RED; GREEN stays behind TEXT'
|
||||
at 12 72
|
||||
printf 'TEXT'
|
||||
at 12 72
|
||||
kitty 'a=p,q=2,C=1,i=202,p=2,x=0,y=0,w=16,h=16,c=4,r=2,z=-1'
|
||||
at 14 72
|
||||
kitty 'a=p,q=2,C=1,i=201,p=3,x=0,y=0,w=16,h=16,c=4,r=2,z=1'
|
||||
at 14 74
|
||||
kitty 'a=p,q=2,C=1,i=203,p=4,x=0,y=0,w=16,h=16,c=4,r=2,z=2'
|
||||
|
||||
# SIXEL is temporal: a later image covers text, while later text destroys the
|
||||
# affected image cells. Both results should match on Kitty and SIXEL output.
|
||||
at 18 2
|
||||
printf '4. SIXEL after text: folder covers FOLDER'
|
||||
at 18 72
|
||||
printf 'FOLDER'
|
||||
at 18 72
|
||||
sixel_folder
|
||||
at 21 2
|
||||
printf '5. SIXEL before text: TEXT WINS, no folder remnant'
|
||||
at 21 72
|
||||
sixel_folder
|
||||
at 21 72
|
||||
printf 'TEXT WINS'
|
||||
|
||||
at 23 2
|
||||
printf 'Take one screenshot, then press Enter to return to the shell.'
|
||||
printf '\033[?25h'
|
||||
IFS= read -r _image_edge_reply
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/bin/sh
|
||||
|
||||
# One-screen visual check for tmux image input and layering. Run this inside a
|
||||
# tmux pane attached to Kitty, WezTerm, Microsoft Terminal, or a basic terminal.
|
||||
|
||||
if [ -z "$TMUX" ]; then
|
||||
echo "Run this script inside tmux." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
columns=$(tput cols 2>/dev/null || echo 0)
|
||||
lines=$(tput lines 2>/dev/null || echo 0)
|
||||
if [ "$columns" -lt 72 ] || [ "$lines" -lt 23 ]; then
|
||||
echo "This test needs at least 72 columns and 23 rows." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
at()
|
||||
{
|
||||
printf '\033[%s;%sH' "$1" "$2"
|
||||
}
|
||||
|
||||
kitty()
|
||||
{
|
||||
printf '\033_G%s\033\\' "$1"
|
||||
}
|
||||
|
||||
# Transmit a 16x16 solid RGBA texture. A one-pixel texture is unsuitable here
|
||||
# because Kitty filters its transparent texture border into scaled edges.
|
||||
kitty_solid()
|
||||
{
|
||||
id=$1
|
||||
rgba=$2
|
||||
payload=$(i=0; while [ "$i" -lt 256 ]; do
|
||||
printf "$rgba"
|
||||
i=$((i + 1))
|
||||
done | base64 | tr -d '\n')
|
||||
kitty "a=t,q=2,f=32,s=16,v=16,i=$id;$payload"
|
||||
}
|
||||
|
||||
# A small 26x26 transparent SIXEL folder, used by the supplied bptato tests.
|
||||
sixel_folder()
|
||||
{
|
||||
printf '\033P0;1q"1;1;26;26#0;2;50;50;50#1;2;0;0;0#2;2;49;49;49#3;2;33;33;33#4;2;47;47;47#5;2;44;44;44#6;2;48;48;48#2?O?C!5?S!4?__?_?__$#0?_wW!5KGwo_-#0?~~!8?!10@BB}w$#2!12?a_A?A?A?A??@E-#0?~~!7?KK~~KK!7?~~$#2!9?K!6?K-#0?F^[owowowowowowowowooS^F$#2?G!10?@@!7?G??G-\033\\'
|
||||
}
|
||||
|
||||
feature_info=$(tmux display-message -p \
|
||||
'term=#{client_termname} features=#{client_termfeatures} cells=#{client_cell_width}x#{client_cell_height}')
|
||||
|
||||
printf '\033[2J\033[H\033[?25l'
|
||||
at 1 2
|
||||
printf 'tmux image visual check'
|
||||
at 2 2
|
||||
printf '%s' "$feature_info"
|
||||
|
||||
# Red is placed after its text at a positive Kitty z-index, so it must cover
|
||||
# the text. Green is placed below the text at a negative Kitty z-index.
|
||||
kitty_solid 101 '\377\000\000\377'
|
||||
kitty_solid 102 '\000\377\000\377'
|
||||
kitty_solid 103 '\000\000\377\377'
|
||||
kitty_solid 104 '\377\000\000\177'
|
||||
|
||||
at 4 2
|
||||
printf '1. positive Kitty z: RED must hide TEXT UNDER at right'
|
||||
at 4 62
|
||||
printf 'TEXT UNDER'
|
||||
at 4 60
|
||||
kitty 'a=p,q=2,C=1,i=101,p=1,x=1,y=1,w=14,h=14,c=18,r=2,z=1'
|
||||
|
||||
at 7 2
|
||||
printf '2. negative Kitty z: GREEN remains behind TEXT ON TOP'
|
||||
at 7 60
|
||||
kitty 'a=p,q=2,C=1,i=102,p=2,x=1,y=1,w=14,h=14,c=18,r=2,z=-1'
|
||||
at 7 62
|
||||
printf 'TEXT ON TOP'
|
||||
|
||||
at 10 2
|
||||
printf '3. Kitty z overlap: RED left, BLUE on top at right'
|
||||
at 11 55
|
||||
kitty 'a=p,q=2,C=1,i=101,p=3,x=1,y=1,w=14,h=14,c=18,r=3,z=1'
|
||||
at 11 64
|
||||
kitty 'a=p,q=2,C=1,i=103,p=4,x=1,y=1,w=14,h=14,c=9,r=3,z=2'
|
||||
|
||||
at 15 2
|
||||
printf '4. alpha: half-transparent RED, dithered on SIXEL'
|
||||
at 15 60
|
||||
kitty 'a=p,q=2,C=1,i=104,p=5,x=1,y=1,w=14,h=14,c=18,r=2,z=1'
|
||||
|
||||
# SIXEL semantics are temporal: later text destroys a SIXEL image, while a
|
||||
# later SIXEL image appears over existing text.
|
||||
at 18 2
|
||||
printf '5. SIXEL after text: folder covers FOLDER at right'
|
||||
at 18 65
|
||||
printf 'FOLDER'
|
||||
at 18 65
|
||||
sixel_folder
|
||||
|
||||
at 21 2
|
||||
printf '6. SIXEL before text: no folder remains behind TEXT WINS'
|
||||
at 21 65
|
||||
sixel_folder
|
||||
at 21 65
|
||||
printf 'TEXT WINS'
|
||||
|
||||
at 23 2
|
||||
printf 'Take one screenshot, then press Enter to return to the shell.'
|
||||
at 23 66
|
||||
printf '\033[?25h'
|
||||
IFS= read -r _image_visual_reply
|
||||
@@ -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-\
|
||||
Executable
+44
@@ -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"
|
||||
@@ -64,9 +64,65 @@ static const struct tty_acs_entry tty_acs_table[] = {
|
||||
{ '{', "\317\200" }, /* greek pi */
|
||||
{ '|', "\342\211\240" }, /* not-equal */
|
||||
{ '}', "\302\243" }, /* UK pound sign */
|
||||
{ '~', "\302\267" } /* bullet */
|
||||
{ '~', "\302\267" }, /* bullet */
|
||||
#ifdef ENABLE_IMAGES
|
||||
{ TTY_ACS_IMAGE_SHADE_LIGHT, "\342\226\221" },
|
||||
{ TTY_ACS_IMAGE_SHADE_MEDIUM, "\342\226\222" },
|
||||
{ TTY_ACS_IMAGE_SHADE_DARK, "\342\226\223" },
|
||||
{ TTY_ACS_IMAGE_BLOCK, "\342\226\210" },
|
||||
{ TTY_ACS_IMAGE_HALF_UPPER, "\342\226\200" },
|
||||
{ TTY_ACS_IMAGE_HALF_LOWER, "\342\226\204" },
|
||||
{ TTY_ACS_IMAGE_HALF_LEFT, "\342\226\214" },
|
||||
{ TTY_ACS_IMAGE_HALF_RIGHT, "\342\226\220" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, "\342\226\226" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT, "\342\226\227" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT, "\342\226\230" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT,
|
||||
"\342\226\231" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT, "\342\226\232" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT,
|
||||
"\342\226\233" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT,
|
||||
"\342\226\234" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, "\342\226\235" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT, "\342\226\236" },
|
||||
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT,
|
||||
"\342\226\237" }
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
static char tty_acs_image_sextants[60][5];
|
||||
|
||||
static void
|
||||
tty_acs_image_sextants_init(void)
|
||||
{
|
||||
u_int cp, i;
|
||||
|
||||
if (tty_acs_image_sextants[0][0] != '\0')
|
||||
return;
|
||||
for (i = 0; i < nitems(tty_acs_image_sextants); i++) {
|
||||
cp = 0x1fb00 + i;
|
||||
tty_acs_image_sextants[i][0] = 0xf0 | (cp >> 18);
|
||||
tty_acs_image_sextants[i][1] = 0x80 | ((cp >> 12) & 0x3f);
|
||||
tty_acs_image_sextants[i][2] = 0x80 | ((cp >> 6) & 0x3f);
|
||||
tty_acs_image_sextants[i][3] = 0x80 | (cp & 0x3f);
|
||||
}
|
||||
}
|
||||
|
||||
u_char
|
||||
tty_acs_image_sextant(u_int mask)
|
||||
{
|
||||
if (mask == 0 || mask == 21 || mask == 42 || mask >= 63)
|
||||
return (0);
|
||||
if (mask < 21)
|
||||
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 1);
|
||||
if (mask < 42)
|
||||
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 2);
|
||||
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 3);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Table mapping UTF-8 to ACS entries. */
|
||||
struct tty_acs_reverse_entry {
|
||||
const char *string;
|
||||
@@ -239,6 +295,15 @@ tty_acs_get(struct tty *tty, u_char ch)
|
||||
return (&tty->term->acs[ch][0]);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (ch >= TTY_ACS_IMAGE_SEXTANT_FIRST &&
|
||||
ch <= TTY_ACS_IMAGE_SEXTANT_LAST) {
|
||||
tty_acs_image_sextants_init();
|
||||
return (tty_acs_image_sextants[
|
||||
ch - TTY_ACS_IMAGE_SEXTANT_FIRST]);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Otherwise look up the UTF-8 translation. */
|
||||
entry = bsearch(&ch, tty_acs_table, nitems(tty_acs_table),
|
||||
sizeof tty_acs_table[0], tty_acs_cmp);
|
||||
|
||||
+24
-5
@@ -122,6 +122,10 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
|
||||
struct grid *gd = s->grid;
|
||||
const struct grid_cell *gcp;
|
||||
struct grid_cell gc, ngc, last;
|
||||
#ifdef ENABLE_IMAGES
|
||||
struct grid_cell image_gc;
|
||||
int image_status;
|
||||
#endif
|
||||
struct grid_line *gl;
|
||||
u_int i, j, last_i, cx, ex, width;
|
||||
u_int cellsize, bg;
|
||||
@@ -251,20 +255,35 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
|
||||
|
||||
if (px >= ex || i >= ex - px) {
|
||||
/* Outside the area being drawn. */
|
||||
empty = nx - i;
|
||||
gcp = &grid_default_cell;
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_status = image_get_fallback_at(tty, s, px + i,
|
||||
py, gcp, &image_gc, style_ctx);
|
||||
if (image_status == 1) {
|
||||
gcp = &image_gc;
|
||||
empty = 0;
|
||||
} else if (image_status == -1)
|
||||
empty = 1;
|
||||
else
|
||||
#endif
|
||||
empty = nx - i;
|
||||
} else {
|
||||
/* Get the current cell. */
|
||||
grid_view_get_cell(gd, px + i, py, &gc);
|
||||
gcp = &gc;
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (image_get_fallback_at(tty, s, px + i, py, &gc,
|
||||
&image_gc, style_ctx) == 1)
|
||||
gcp = &image_gc;
|
||||
#endif
|
||||
|
||||
/* Work out empty cells. */
|
||||
empty = tty_draw_line_get_empty(&gc, &last,
|
||||
nx - i);
|
||||
empty = tty_draw_line_get_empty(gcp, &last, nx - i);
|
||||
if (empty != 0)
|
||||
gcp = &gc;
|
||||
;
|
||||
else {
|
||||
/* Update for codeset if needed. */
|
||||
gcp = tty_check_codeset(tty, &gc);
|
||||
gcp = tty_check_codeset(tty, gcp);
|
||||
|
||||
/* And for selection. */
|
||||
if (gcp->flags & GRID_FLAG_SELECTED) {
|
||||
|
||||
+72
-3
@@ -1,4 +1,4 @@
|
||||
/* $OpenBSD: tty-features.c,v 1.46 2026/09/24 08:16:13 nicm Exp $ */
|
||||
/* $OpenBSD: tty-features.c,v 1.45 2026/09/22 14:10:26 nicm Exp $ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||
@@ -236,9 +236,11 @@ static const struct tty_feature tty_feature_strikethrough = {
|
||||
0
|
||||
};
|
||||
|
||||
#define TTY_FEATURE_SYNC "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;"
|
||||
|
||||
/* Terminal supports synchronized updates. */
|
||||
static const char *const tty_feature_sync_capabilities[] = {
|
||||
"Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;",
|
||||
TTY_FEATURE_SYNC,
|
||||
NULL
|
||||
};
|
||||
static const struct tty_feature tty_feature_sync = {
|
||||
@@ -369,6 +371,50 @@ static const struct tty_feature tty_feature_sixel = {
|
||||
TERM_SIXEL
|
||||
};
|
||||
|
||||
/* Terminal has Kitty graphics protocol Unicode placeholder capability. */
|
||||
static const char *const tty_feature_kitty_capabilities[] = {
|
||||
"Kty",
|
||||
TTY_FEATURE_SYNC,
|
||||
NULL
|
||||
};
|
||||
static const struct tty_feature tty_feature_kitty = {
|
||||
"kitty",
|
||||
tty_feature_kitty_capabilities,
|
||||
TERM_KITTY
|
||||
};
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
static const char *const tty_feature_image_capabilities[] = {
|
||||
NULL
|
||||
};
|
||||
static const struct tty_feature tty_feature_image_quadrants = {
|
||||
"image-quadrants",
|
||||
tty_feature_image_capabilities,
|
||||
TERM_IMAGE_QUADRANTS
|
||||
};
|
||||
static const struct tty_feature tty_feature_image_sextants = {
|
||||
"image-sextants",
|
||||
tty_feature_image_capabilities,
|
||||
TERM_IMAGE_SEXTANTS
|
||||
};
|
||||
|
||||
/*
|
||||
* Terminal moves SIXEL or Kitty image content along with the rest of a
|
||||
* scrolling region, rather than needing it redrawn after every scroll.
|
||||
* There is no way to ask a terminal this, and it does not correlate with
|
||||
* DECSLRM/margins support - confirmed by direct testing that mintty
|
||||
* scrolls text within a margin-bounded region correctly but drops sixel
|
||||
* content placed there, while WezTerm and Windows Terminal move it
|
||||
* correctly - so this is granted per terminal individually rather than
|
||||
* assumed from any other capability.
|
||||
*/
|
||||
static const struct tty_feature tty_feature_imagescroll = {
|
||||
"imagescroll",
|
||||
tty_feature_image_capabilities,
|
||||
TERM_IMAGESCROLL
|
||||
};
|
||||
#endif
|
||||
|
||||
/* Terminal supports the OSC 9;4 progress bar. */
|
||||
static const char *const tty_feature_progressbar_capabilities[] = {
|
||||
"Spb=\\E]9;4;%p1%d;%p2%d\\E\\\\",
|
||||
@@ -399,6 +445,12 @@ static const struct tty_feature *const tty_features[] = {
|
||||
&tty_feature_extkeys,
|
||||
&tty_feature_focus,
|
||||
&tty_feature_ignorefkeys,
|
||||
&tty_feature_kitty,
|
||||
#ifdef ENABLE_IMAGES
|
||||
&tty_feature_image_quadrants,
|
||||
&tty_feature_image_sextants,
|
||||
&tty_feature_imagescroll,
|
||||
#endif
|
||||
&tty_feature_margins,
|
||||
&tty_feature_mouse,
|
||||
&tty_feature_osc7,
|
||||
@@ -637,7 +689,9 @@ tty_default_features(struct client *c, const char *name, u_int version)
|
||||
"extkeys,"
|
||||
"focus,"
|
||||
"hyperlinks,"
|
||||
"imagescroll,"
|
||||
"margins,"
|
||||
"sixel,"
|
||||
"usstyle"
|
||||
},
|
||||
{ .name = "ghostty",
|
||||
@@ -646,13 +700,28 @@ tty_default_features(struct client *c, const char *name, u_int version)
|
||||
"cstyle,"
|
||||
"extkeys,"
|
||||
"focus,"
|
||||
"imagescroll,"
|
||||
"margins,"
|
||||
"overline,"
|
||||
"hyperlinks,"
|
||||
"osc7,"
|
||||
"sync,"
|
||||
"usstyle,"
|
||||
"progressbar"
|
||||
"progressbar,"
|
||||
"kitty"
|
||||
},
|
||||
{ .name = "kitty",
|
||||
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
|
||||
"ccolour,"
|
||||
"cstyle,"
|
||||
"extkeys,"
|
||||
"focus,"
|
||||
"hyperlinks,"
|
||||
"imagescroll,"
|
||||
"osc7,"
|
||||
"sync,"
|
||||
"usstyle,"
|
||||
"kitty"
|
||||
},
|
||||
{ .name = "Rio",
|
||||
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
|
||||
|
||||
@@ -729,6 +729,7 @@ tty_keys_winsz(struct tty *tty, const char *buf, size_t len, size_t *size)
|
||||
char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0;
|
||||
tty_set_size(tty, tty->sx, tty->sy, char_x, char_y);
|
||||
tty_invalidate(tty);
|
||||
recalculate_sizes();
|
||||
|
||||
tty->flags &= ~TTY_WINSIZEQUERY;
|
||||
*size = end + 1;
|
||||
|
||||
@@ -531,6 +531,7 @@ tty_term_apply_overrides(struct tty_term *term)
|
||||
tty_term_validate(term);
|
||||
}
|
||||
|
||||
/* Mark terminals whose Ms capability does not contain a clipboard argument. */
|
||||
static void
|
||||
tty_term_validate(struct tty_term *term)
|
||||
{
|
||||
|
||||
@@ -67,11 +67,6 @@ static void tty_emulate_repeat(struct tty *, enum tty_code_code,
|
||||
enum tty_code_code, u_int);
|
||||
static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int);
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
static void tty_write_one(void (*)(struct tty *, const struct tty_ctx *),
|
||||
struct client *, struct tty_ctx *);
|
||||
#endif
|
||||
|
||||
#define tty_use_margin(tty) \
|
||||
(tty->term->flags & TERM_DECSLRM)
|
||||
#define tty_full_width(tty, ctx) \
|
||||
@@ -162,10 +157,21 @@ tty_resize(struct tty *tty)
|
||||
void
|
||||
tty_set_size(struct tty *tty, u_int sx, u_int sy, u_int xpixel, u_int ypixel)
|
||||
{
|
||||
#ifdef ENABLE_IMAGES
|
||||
int geometry_changed;
|
||||
|
||||
geometry_changed = (tty->xpixel != xpixel || tty->ypixel != ypixel);
|
||||
#endif
|
||||
tty->sx = sx;
|
||||
tty->sy = sy;
|
||||
tty->xpixel = xpixel;
|
||||
tty->ypixel = ypixel;
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (geometry_changed) {
|
||||
image_tty_geometry_changed(tty);
|
||||
server_redraw_client(tty->client);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -283,6 +289,10 @@ tty_open(struct tty *tty, char **cause)
|
||||
}
|
||||
tty->flags |= TTY_OPENED;
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_tty_update(tty);
|
||||
#endif
|
||||
|
||||
tty->flags &= ~(TTY_NOCURSOR|TTY_FREEZE|TTY_BLOCK|TTY_TIMER);
|
||||
|
||||
event_set(&tty->event_in, c->fd, EV_PERSIST|EV_READ,
|
||||
@@ -527,6 +537,9 @@ tty_close(struct tty *tty)
|
||||
tty_stop_tty(tty);
|
||||
|
||||
if (tty->flags & TTY_OPENED) {
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_tty_free(tty, 1);
|
||||
#endif
|
||||
evbuffer_free(tty->in);
|
||||
event_del(&tty->event_in);
|
||||
evbuffer_free(tty->out);
|
||||
@@ -543,15 +556,25 @@ void
|
||||
tty_free(struct tty *tty)
|
||||
{
|
||||
tty_close(tty);
|
||||
#ifdef ENABLE_IMAGES
|
||||
image_tty_free(tty, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
tty_update_features(struct tty *tty)
|
||||
{
|
||||
struct client *c = tty->client;
|
||||
int changed = 0;
|
||||
|
||||
if (tty_apply_features(tty->term))
|
||||
if (tty_apply_features(tty->term)) {
|
||||
tty_term_apply_overrides(tty->term);
|
||||
changed = 1;
|
||||
}
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (image_tty_update(tty))
|
||||
changed = 1;
|
||||
#endif
|
||||
|
||||
if (tty_use_margin(tty))
|
||||
tty_putcode(tty, TTYC_ENMG);
|
||||
@@ -564,7 +587,22 @@ tty_update_features(struct tty *tty)
|
||||
/*
|
||||
* Features might have changed since the first draw during attach. For
|
||||
* example, this happens when DA responses are received.
|
||||
*
|
||||
* Only redraw when something actually did change. This function is
|
||||
* called for every DA, secondary DA and extended DA answer, and from
|
||||
* the start timer when none arrive - answers which usually just
|
||||
* confirm what is already known, either from a previous answer or
|
||||
* from terminal-features in the configuration. The redraw is not
|
||||
* free: it repaints the pane from tmux's own grid, which discards
|
||||
* anything the pane put on the terminal that tmux does not model,
|
||||
* notably an image written through DCS passthrough. That makes an
|
||||
* unnecessary redraw here visible to the user as an image that
|
||||
* appears and then vanishes a moment later, once per client, with no
|
||||
* way for the application to detect it and redraw.
|
||||
*/
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
server_redraw_client(c);
|
||||
|
||||
tty_invalidate(tty);
|
||||
@@ -1455,59 +1493,6 @@ tty_check_codeset(struct tty *tty, const struct grid_cell *gc)
|
||||
return (&new);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
/* Update context for client. */
|
||||
static int
|
||||
tty_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
|
||||
{
|
||||
struct window_pane *wp = ttyctx->arg;
|
||||
|
||||
if (c->session->curw->window != wp->window)
|
||||
return (0);
|
||||
if (wp->layout_cell == NULL)
|
||||
return (0);
|
||||
|
||||
if (tty_window_offset(&c->tty, &ttyctx->wox, &ttyctx->woy, &ttyctx->wsx,
|
||||
&ttyctx->wsy))
|
||||
ttyctx->flags |= TTY_CTX_WINDOW_BIGGER;
|
||||
else
|
||||
ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER;
|
||||
|
||||
ttyctx->yoff = ttyctx->ryoff = wp->yoff;
|
||||
if (status_at_line(c) == 0)
|
||||
ttyctx->yoff += status_line_size(c);
|
||||
|
||||
return (1);
|
||||
}
|
||||
|
||||
void
|
||||
tty_draw_images(struct client *c, struct window_pane *wp)
|
||||
{
|
||||
struct image *im;
|
||||
struct tty_ctx ttyctx;
|
||||
|
||||
TAILQ_FOREACH(im, &wp->screen->images, entry) {
|
||||
memset(&ttyctx, 0, sizeof ttyctx);
|
||||
|
||||
/* Set the client independent properties. */
|
||||
ttyctx.ocx = im->px;
|
||||
ttyctx.ocy = im->py;
|
||||
|
||||
ttyctx.orlower = wp->screen->rlower;
|
||||
ttyctx.orupper = wp->screen->rupper;
|
||||
|
||||
ttyctx.xoff = ttyctx.rxoff = wp->xoff;
|
||||
ttyctx.sx = wp->sx;
|
||||
ttyctx.sy = wp->sy;
|
||||
|
||||
ttyctx.image = im;
|
||||
ttyctx.arg = wp;
|
||||
ttyctx.set_client_cb = tty_set_client_cb;
|
||||
ttyctx.flags |= TTY_CTX_INVISIBLE_PANES;
|
||||
tty_write_one(tty_cmd_sixelimage, c, &ttyctx);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void
|
||||
tty_sync_start(struct tty *tty)
|
||||
@@ -1583,19 +1568,6 @@ tty_write(void (*cmdfn)(struct tty *, const struct tty_ctx *),
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
/* Only write to the incoming tty instead of every client. */
|
||||
static void
|
||||
tty_write_one(void (*cmdfn)(struct tty *, const struct tty_ctx *),
|
||||
struct client *c, struct tty_ctx *ctx)
|
||||
{
|
||||
if (ctx->set_client_cb == NULL)
|
||||
return;
|
||||
if ((ctx->set_client_cb(ctx, c)) == 1)
|
||||
cmdfn(&c->tty, ctx);
|
||||
}
|
||||
#endif
|
||||
|
||||
void
|
||||
tty_cmd_insertcharacter(struct tty *tty, const struct tty_ctx *ctx)
|
||||
{
|
||||
@@ -1730,10 +1702,17 @@ tty_cmd_reverseindex(struct tty *tty, const struct tty_ctx *ctx)
|
||||
!tty_term_has(tty->term, TTYC_RIN)) ||
|
||||
ctx->sx == 1 ||
|
||||
ctx->sy == 1) {
|
||||
#ifdef ENABLE_IMAGES
|
||||
redraw_image_scroll_result(tty, ctx, 1);
|
||||
#endif
|
||||
tty_redraw_region(tty, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
redraw_image_scroll_result(tty, ctx, 0);
|
||||
#endif
|
||||
|
||||
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
|
||||
|
||||
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
|
||||
@@ -1758,10 +1737,17 @@ tty_cmd_linefeed(struct tty *tty, const struct tty_ctx *ctx)
|
||||
!tty_term_has(tty->term, TTYC_CSR) ||
|
||||
ctx->sx == 1 ||
|
||||
ctx->sy == 1) {
|
||||
#ifdef ENABLE_IMAGES
|
||||
redraw_image_scroll_result(tty, ctx, 1);
|
||||
#endif
|
||||
tty_redraw_region(tty, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
redraw_image_scroll_result(tty, ctx, 0);
|
||||
#endif
|
||||
|
||||
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
|
||||
|
||||
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
|
||||
@@ -1796,10 +1782,17 @@ tty_cmd_scrollup(struct tty *tty, const struct tty_ctx *ctx)
|
||||
!tty_term_has(tty->term, TTYC_CSR) ||
|
||||
ctx->sx == 1 ||
|
||||
ctx->sy == 1) {
|
||||
#ifdef ENABLE_IMAGES
|
||||
redraw_image_scroll_result(tty, ctx, 1);
|
||||
#endif
|
||||
tty_redraw_region(tty, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
redraw_image_scroll_result(tty, ctx, 0);
|
||||
#endif
|
||||
|
||||
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
|
||||
|
||||
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
|
||||
@@ -1834,10 +1827,17 @@ tty_cmd_scrolldown(struct tty *tty, const struct tty_ctx *ctx)
|
||||
!tty_term_has(tty->term, TTYC_RIN)) ||
|
||||
ctx->sx == 1 ||
|
||||
ctx->sy == 1) {
|
||||
#ifdef ENABLE_IMAGES
|
||||
redraw_image_scroll_result(tty, ctx, 1);
|
||||
#endif
|
||||
tty_redraw_region(tty, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
redraw_image_scroll_result(tty, ctx, 0);
|
||||
#endif
|
||||
|
||||
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
|
||||
|
||||
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
|
||||
@@ -2029,58 +2029,6 @@ tty_cmd_rawstring(struct tty *tty, const struct tty_ctx *ctx)
|
||||
tty_invalidate(tty);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SIXEL
|
||||
void
|
||||
tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx)
|
||||
{
|
||||
struct image *im = ctx->image;
|
||||
struct sixel_image *si = im->data;
|
||||
struct sixel_image *new;
|
||||
char *data;
|
||||
size_t size;
|
||||
u_int cx = ctx->ocx, cy = ctx->ocy, sx, sy;
|
||||
u_int i, j, x, y, rx, ry;
|
||||
int fallback = 0;
|
||||
|
||||
if ((~tty->term->flags & TERM_SIXEL) &&
|
||||
!tty_term_has(tty->term, TTYC_SXL))
|
||||
fallback = 1;
|
||||
if (tty->xpixel == 0 || tty->ypixel == 0)
|
||||
fallback = 1;
|
||||
|
||||
sixel_size_in_cells(si, &sx, &sy);
|
||||
log_debug("%s: image is %ux%u", __func__, sx, sy);
|
||||
if (!tty_clamp_area(tty, ctx, cx, cy, sx, sy, &i, &j, &x, &y, &rx, &ry))
|
||||
return;
|
||||
log_debug("%s: clamping to %u,%u-%u,%u", __func__, i, j, rx, ry);
|
||||
|
||||
if (fallback == 1) {
|
||||
data = xstrdup(im->fallback);
|
||||
size = strlen(data);
|
||||
} else {
|
||||
new = sixel_scale(si, tty->xpixel, tty->ypixel, i, j, rx, ry, 0);
|
||||
if (new == NULL)
|
||||
return;
|
||||
|
||||
data = sixel_print(new, si, &size);
|
||||
}
|
||||
if (data != NULL) {
|
||||
log_debug("%s: %zu bytes: %s", __func__, size, data);
|
||||
tty_region_off(tty);
|
||||
tty_margin_off(tty);
|
||||
tty_cursor(tty, x, y);
|
||||
|
||||
tty->flags |= TTY_NOBLOCK;
|
||||
tty_add(tty, data, size);
|
||||
tty_invalidate(tty);
|
||||
free(data);
|
||||
}
|
||||
|
||||
if (fallback == 0)
|
||||
sixel_free(new);
|
||||
}
|
||||
#endif
|
||||
|
||||
void
|
||||
tty_cmd_syncstart(struct tty *tty, const struct tty_ctx *ctx)
|
||||
{
|
||||
|
||||
@@ -374,6 +374,10 @@ utf8_add_to_width_cache(const char *s)
|
||||
wc_end = wc_start;
|
||||
}
|
||||
|
||||
/*
|
||||
* wc_end may be WCHAR_MAX, so incrementing wc past it would
|
||||
* overflow (wc_start <= wc_end is already guaranteed above).
|
||||
*/
|
||||
wc = wc_start;
|
||||
for (;;) {
|
||||
utf8_insert_width_cache(wc, width);
|
||||
|
||||
+132
@@ -73,6 +73,10 @@ static void window_copy_write_line(struct window_mode_entry *,
|
||||
struct screen_write_ctx *, u_int);
|
||||
static void window_copy_write_lines(struct window_mode_entry *,
|
||||
struct screen_write_ctx *, u_int, u_int);
|
||||
#ifdef ENABLE_IMAGES
|
||||
static void window_copy_update_image_refresh(struct window_copy_mode_data *);
|
||||
static int window_copy_visible_has_images(struct window_copy_mode_data *);
|
||||
#endif
|
||||
static char *window_copy_match_at_cursor(struct window_copy_mode_data *);
|
||||
static void window_copy_scroll_to(struct window_mode_entry *, u_int, u_int,
|
||||
int);
|
||||
@@ -279,6 +283,10 @@ struct window_copy_mode_data {
|
||||
|
||||
u_int oy; /* number of lines scrolled up */
|
||||
|
||||
u_int image_base; /* hsize - oy images were last drawn for */
|
||||
int image_base_set;
|
||||
int image_refresh; /* current redraw needs image refresh */
|
||||
|
||||
u_int selx; /* beginning of selection */
|
||||
u_int sely;
|
||||
|
||||
@@ -636,6 +644,9 @@ window_copy_init(struct window_mode_entry *wme,
|
||||
data->my = screen_hsize(data->backing) + data->cy - data->oy;
|
||||
data->showmark = 0;
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
window_copy_update_image_refresh(data);
|
||||
#endif
|
||||
screen_write_start(&ctx, &data->screen);
|
||||
for (i = 0; i < screen_size_y(&data->screen); i++)
|
||||
window_copy_write_line(wme, &ctx, i);
|
||||
@@ -1198,6 +1209,9 @@ window_copy_size_changed(struct window_mode_entry *wme)
|
||||
window_copy_clear_selection(wme);
|
||||
window_copy_clear_marks(wme);
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
window_copy_update_image_refresh(data);
|
||||
#endif
|
||||
screen_write_start(&ctx, s);
|
||||
window_copy_write_lines(wme, &ctx, 0, screen_size_y(s));
|
||||
screen_write_stop(&ctx);
|
||||
@@ -5224,6 +5238,34 @@ window_copy_write_one(struct window_mode_entry *wme,
|
||||
for (fx = 0; fx < nx; fx++) {
|
||||
grid_get_cell(gd, fx, fy, &gc);
|
||||
if (fx + gc.data.width <= nx) {
|
||||
#ifdef ENABLE_IMAGES
|
||||
/*
|
||||
* Write image-covered cells directly into the grid,
|
||||
* skipping both window_copy_update_style() (so a
|
||||
* selection, current-line or search-mark highlight
|
||||
* never sweeps visibly over the image before it is
|
||||
* recomposited separately - see the
|
||||
* image_redraw_area() call below) and
|
||||
* screen_write_cell(), whose built-in
|
||||
* screen_write_image_damage() call (screen-write.c)
|
||||
* fires on every write regardless of whether
|
||||
* anything actually changed, which would needlessly
|
||||
* re-damage - and so retransmit - the image on every
|
||||
* redraw. The cell must still land in the grid as
|
||||
* normal: a non-graphical client's ASCII fallback for
|
||||
* the image is an ordinary character here, not
|
||||
* something recomposited separately, and depends on
|
||||
* this write the same as any other cell.
|
||||
*/
|
||||
if (image_grid_check_area(gd, fx, fy, gc.data.width,
|
||||
1)) {
|
||||
grid_view_set_cell(ctx->s->grid, px + fx, py,
|
||||
&gc);
|
||||
screen_write_cursormove(ctx,
|
||||
px + fx + gc.data.width, py, 0);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
window_copy_update_style(wme, fx, fy, &gc, mgc, cgc,
|
||||
mkgc, clgc);
|
||||
if (gc.flags & GRID_FLAG_PADDING) {
|
||||
@@ -5472,6 +5514,29 @@ window_copy_write_line(struct window_mode_entry *wme,
|
||||
window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py,
|
||||
content_sx, &mgc, &cgc, &mkgc, &clgc);
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
/*
|
||||
* Copy the backing line's image layers separately from its text
|
||||
* cells. This is not implied by the text cells just written above:
|
||||
* those go via the pane's normal (frequently fast, direct-write)
|
||||
* path, which knows nothing about image content.
|
||||
*
|
||||
* Only report it as needing a redraw when the view has actually
|
||||
* moved (data->image_refresh, set once per window_copy_redraw_lines
|
||||
* call) - text writes never touch image-covered cells (see
|
||||
* window_copy_write_one), so if the view is unmoved this row's
|
||||
* images are already exactly as they should be and redrawing them
|
||||
* anyway just flashes the image on every unrelated redraw (e.g.
|
||||
* every step of a selection drag) for no visible benefit.
|
||||
*/
|
||||
image_grid_free_line(s->grid,
|
||||
&s->grid->linedata[s->grid->hsize + py]);
|
||||
image_grid_copy_area(s->grid, width, s->grid->hsize + py,
|
||||
data->backing->grid, 0, hsize - data->oy + py, content_sx, 1);
|
||||
if (data->image_refresh)
|
||||
image_redraw_area(ctx, width, py, content_sx, 1);
|
||||
#endif
|
||||
|
||||
if (py == 0 && s->rupper < s->rlower && !data->hide_position) {
|
||||
value = options_get_string(oo, "copy-mode-position-format");
|
||||
if (*value != '\0') {
|
||||
@@ -5532,6 +5597,55 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y)
|
||||
window_copy_redraw_lines(wme, start, end - start + 1);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
/*
|
||||
* Only rows whose underlying history position has moved since the last
|
||||
* call need their images recomposited - most redraws are just a selection
|
||||
* or cursor-line style change with the view otherwise unmoved, and that
|
||||
* never touches image-covered cells (see window_copy_write_one()), so
|
||||
* redrawing images for it is needless: on a fast drag it is visible as the
|
||||
* image briefly flashing on every step even though nothing about it
|
||||
* actually changed.
|
||||
*
|
||||
* Every place that calls window_copy_write_line()/window_copy_write_lines()
|
||||
* must call this first - it is not implied by them, since some (the
|
||||
* initial full-screen draw on entering copy mode, window_copy_scroll_up(),
|
||||
* window_copy_scroll_down()) write directly rather than going through
|
||||
* window_copy_redraw_lines().
|
||||
*/
|
||||
static void
|
||||
window_copy_update_image_refresh(struct window_copy_mode_data *data)
|
||||
{
|
||||
u_int base;
|
||||
|
||||
base = screen_hsize(data->backing) - data->oy;
|
||||
data->image_refresh = !data->image_base_set || base != data->image_base;
|
||||
data->image_base = base;
|
||||
data->image_base_set = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Whether any part of the currently visible backing range carries image
|
||||
* data. A terminal's line insert/delete only shifts character cells - the
|
||||
* pixels of a sixel or Kitty image already on screen stay exactly where
|
||||
* they were sent, so a scroll that uses that fast path (see
|
||||
* window_copy_scroll_up()/window_copy_scroll_down()) leaves stale or
|
||||
* missing image content for every row except the single new one it
|
||||
* explicitly rewrites. When this returns true, callers should fall back to
|
||||
* a full window_copy_redraw_screen() instead, so every visible row's image
|
||||
* is recomposited at its correct new position.
|
||||
*/
|
||||
static int
|
||||
window_copy_visible_has_images(struct window_copy_mode_data *data)
|
||||
{
|
||||
struct grid *gd = data->backing->grid;
|
||||
u_int sy = screen_size_y(&data->screen);
|
||||
|
||||
return (image_grid_check_area(gd, 0, screen_hsize(data->backing) -
|
||||
data->oy, screen_size_x(&data->screen), sy));
|
||||
}
|
||||
#endif
|
||||
|
||||
static void
|
||||
window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny)
|
||||
{
|
||||
@@ -5541,6 +5655,10 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny)
|
||||
struct screen_write_ctx ctx;
|
||||
u_int i;
|
||||
|
||||
#ifdef ENABLE_IMAGES
|
||||
window_copy_update_image_refresh(data);
|
||||
#endif
|
||||
|
||||
if (window_copy_line_number_width(wme) != 0) {
|
||||
screen_write_start(&ctx, &data->screen);
|
||||
for (i = py; i < py + ny; i++)
|
||||
@@ -6929,6 +7047,13 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny)
|
||||
window_copy_redraw_screen(wme);
|
||||
return;
|
||||
}
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (window_copy_visible_has_images(data)) {
|
||||
window_copy_redraw_screen(wme);
|
||||
return;
|
||||
}
|
||||
window_copy_update_image_refresh(data);
|
||||
#endif
|
||||
if (window_copy_line_numbers_active(wme)) {
|
||||
if (window_copy_line_number_mode(wme) !=
|
||||
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {
|
||||
@@ -7000,6 +7125,13 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny)
|
||||
window_copy_redraw_screen(wme);
|
||||
return;
|
||||
}
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (window_copy_visible_has_images(data)) {
|
||||
window_copy_redraw_screen(wme);
|
||||
return;
|
||||
}
|
||||
window_copy_update_image_refresh(data);
|
||||
#endif
|
||||
if (window_copy_line_numbers_active(wme)) {
|
||||
if (window_copy_line_number_mode(wme) !=
|
||||
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* $OpenBSD: window.c,v 1.378 2026/09/24 11:19:39 nicm Exp $ */
|
||||
/* $OpenBSD: window.c,v 1.377 2026/09/21 10:33:16 nicm Exp $ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||
@@ -1544,7 +1544,7 @@ window_pane_destroy(struct window_pane *wp)
|
||||
window_pane_clear_prompt(wp);
|
||||
|
||||
window_pane_free_modes(wp);
|
||||
screen_write_sync_clear_dirty(wp);
|
||||
screen_write_clear_dirty(wp);
|
||||
|
||||
if (wp->fd != -1) {
|
||||
#ifdef HAVE_UTEMPTER
|
||||
@@ -1692,6 +1692,10 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy)
|
||||
|
||||
log_debug("%s: %%%u resize %ux%u", __func__, wp->id, sx, sy);
|
||||
screen_resize(&wp->base, sx, sy, wp->base.saved_grid == NULL);
|
||||
#ifdef ENABLE_IMAGES
|
||||
if (sx > r->osx)
|
||||
image_grid_resize_width(wp->base.grid, sx);
|
||||
#endif
|
||||
|
||||
wme = TAILQ_FIRST(&wp->modes);
|
||||
if (wme != NULL && wme->mode->resize != NULL)
|
||||
@@ -2942,21 +2946,31 @@ window_pane_is_floating_with_hidden(struct window_pane *wp)
|
||||
return (1);
|
||||
}
|
||||
|
||||
/* Report damage for a floating pane, including its border and scrollbar. */
|
||||
/*
|
||||
* Report damage for a floating pane's rectangle, grown by one cell on every
|
||||
* side - a floating pane draws its border frame at xoff-1/yoff-1 through
|
||||
* xoff+sx/yoff+sy (see the "floating" case in screen-redraw.c), one cell
|
||||
* outside its own content area, so damage for just the content area leaves
|
||||
* the frame's previous position undrawn as the pane moves. If a scrollbar
|
||||
* is reserved, its side of the frame is pushed out further still by its
|
||||
* width and padding (also matched in screen-redraw.c), so grow that side
|
||||
* to match.
|
||||
*/
|
||||
static void
|
||||
window_damage_floating_pane(struct window_pane *wp, int xoff, int yoff,
|
||||
int sx, int sy)
|
||||
window_pane_damage_floating(struct window *w, struct window_pane *wp,
|
||||
int xoff, int yoff, int sx, int sy)
|
||||
{
|
||||
struct window *w = wp->window;
|
||||
int x0, x1, y0, y1, sb_left = 0, sb_right = 0;
|
||||
struct style *sb_sy = &wp->scrollbar_style;
|
||||
int x0, x1, y0, y1, sb_left = 0, sb_right = 0;
|
||||
|
||||
if (window_pane_scrollbar_reserve(wp)) {
|
||||
if (w->sb_pos == PANE_SCROLLBARS_LEFT)
|
||||
sb_left = sb_sy->width + sb_sy->pad;
|
||||
sb_left = wp->scrollbar_style.width +
|
||||
wp->scrollbar_style.pad;
|
||||
else
|
||||
sb_right = sb_sy->width + sb_sy->pad;
|
||||
sb_right = wp->scrollbar_style.width +
|
||||
wp->scrollbar_style.pad;
|
||||
}
|
||||
|
||||
x0 = xoff - 1 - sb_left;
|
||||
x1 = xoff + sx + sb_right;
|
||||
y0 = yoff - 1;
|
||||
@@ -2965,16 +2979,75 @@ window_damage_floating_pane(struct window_pane *wp, int xoff, int yoff,
|
||||
x0 = 0;
|
||||
if (y0 < 0)
|
||||
y0 = 0;
|
||||
if (x1 >= x0 && y1 >= y0)
|
||||
redraw_damage_window(w, x0, y0, x1 - x0 + 1U, y1 - y0 + 1U);
|
||||
if (x1 < x0 || y1 < y0)
|
||||
return;
|
||||
redraw_damage_window(w, (u_int)x0, (u_int)y0, (u_int)(x1 - x0) + 1,
|
||||
(u_int)(y1 - y0) + 1);
|
||||
}
|
||||
|
||||
/* Report damage for a floating pane's old and new areas. */
|
||||
void
|
||||
window_redraw_floating_pane(struct window_pane *wp, int oxoff, int oyoff,
|
||||
int osx, int osy)
|
||||
/*
|
||||
* Whether a pane's scrollbar strip - not its whole body - intersects a
|
||||
* window-coordinate rectangle. A reserved scrollbar occupies a strip of
|
||||
* scrollbar_style.width+pad columns just outside the pane's own content
|
||||
* area (see the scrollbar-reserve case in layout_fix_panes(), layout.c),
|
||||
* on whichever side w->sb_pos points to.
|
||||
*/
|
||||
static int
|
||||
window_pane_scrollbar_intersects(struct window *w, struct window_pane *wp,
|
||||
u_int x, u_int y, u_int sx, u_int sy)
|
||||
{
|
||||
window_damage_floating_pane(wp, oxoff, oyoff, osx, osy);
|
||||
window_damage_floating_pane(wp, wp->xoff, wp->yoff, wp->sx, wp->sy);
|
||||
server_status_window(wp->window);
|
||||
int sb_x, sb_w, ix = (int)x, iy = (int)y, isx = (int)sx;
|
||||
int isy = (int)sy;
|
||||
|
||||
if (!window_pane_scrollbar_reserve(wp))
|
||||
return (0);
|
||||
sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad;
|
||||
if (w->sb_pos == PANE_SCROLLBARS_LEFT)
|
||||
sb_x = (int)wp->xoff - sb_w;
|
||||
else
|
||||
sb_x = (int)wp->xoff + (int)wp->sx;
|
||||
|
||||
return (sb_x < ix + isx && sb_x + sb_w > ix &&
|
||||
(int)wp->yoff < iy + isy && (int)wp->yoff + (int)wp->sy > iy);
|
||||
}
|
||||
|
||||
/*
|
||||
* Report damage for only a floating pane's old and new area, rather than
|
||||
* the whole window - a floating pane move or resize only disturbs what it
|
||||
* was covering and what it now covers. Scrollbars aren't covered by the
|
||||
* damage system, so a pane whose *scrollbar strip* (not its whole body)
|
||||
* intersects either area is still flagged directly for a scrollbar redraw.
|
||||
* Checking the whole pane body here, rather than just its narrow scrollbar
|
||||
* strip, meant merely dragging over a pane's ordinary content set
|
||||
* PANE_REDRAWSCROLLBAR on every such pane on every motion event, triggering
|
||||
* a needless scrollbar redraw (and the redraw pass it forces) each time
|
||||
* even though the scrollbar itself never moved.
|
||||
*
|
||||
* Shared by every command that drags a floating pane around by the mouse:
|
||||
* resize-pane's own border drag (cmd-resize-pane.c), move-pane -M's
|
||||
* alternate Alt-drag (cmd-join-pane.c), and split-window/new-pane's
|
||||
* interactive resize of a newly-created floating pane (cmd-split-window.c).
|
||||
*/
|
||||
void
|
||||
window_pane_redraw_floating(struct window *w, struct window_pane *wp,
|
||||
int old_xoff, int old_yoff, int old_sx, int old_sy)
|
||||
{
|
||||
struct window_pane *loop;
|
||||
|
||||
window_pane_damage_floating(w, wp, old_xoff, old_yoff, old_sx,
|
||||
old_sy);
|
||||
window_pane_damage_floating(w, wp, wp->xoff, wp->yoff, wp->sx,
|
||||
wp->sy);
|
||||
|
||||
TAILQ_FOREACH(loop, &w->panes, entry) {
|
||||
if (window_pane_scrollbar_intersects(w, loop,
|
||||
(u_int)old_xoff, (u_int)old_yoff, (u_int)old_sx,
|
||||
(u_int)old_sy) ||
|
||||
window_pane_scrollbar_intersects(w, loop, wp->xoff,
|
||||
wp->yoff, wp->sx, wp->sy))
|
||||
loop->flags |= PANE_REDRAWSCROLLBAR;
|
||||
}
|
||||
|
||||
/* Session status formats may depend on the pane's new geometry. */
|
||||
server_status_window(w);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user