Compare commits

..
Author SHA1 Message Date
Nicholas Marriott e621d088b4 Merge branch 'master' into redraw-damage-rectangles-new 2026-09-25 10:11:51 +01:00
Nicholas Marriott ef881f318c Add --with-socket-path to configure. GitHub issue 5635 from Alexandre Fiori. 2026-09-25 10:07:39 +01:00
Nicholas Marriott 545ba99f8c Tests for 5636. 2026-09-25 09:46:21 +01:00
Nicholas Marriott 8cafc7633e Merge pull request #5633 from arthurscchan/new-fuzzer-2
OSS-Fuzz: Add new fuzzer targets json processing
2026-09-25 07:58:11 +01:00
tmux update bot 46fd3c16a3 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Keep the scroll position in tree mode when the list is rebuilt, GitHub issue 5631 from Kim A Brandt.
2026-09-25 06:40:35 +00:00
nicm 4ae837de68 Keep the scroll position in tree mode when the list is rebuilt, GitHub
issue 5631 from Kim A Brandt.
2026-09-25 06:40:33 +00:00
Arthur Chan 2888c6751c Prepare dict and options
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-09-25 06:20:49 +01:00
Arthur Chan 041f30acbe OSS-Fuzz: Add new fuzzer targets json processing
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-09-25 05:58:37 +01:00
Nicholas Marriott 4f523f41c9 Update CHANGES. 2026-09-24 20:49:41 +01:00
Nicholas Marriott 5a0440b4f8 Test for 5631. 2026-09-24 19:59:45 +01:00
tmux update bot 51593fc9bc Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  As well as tracking modified (dirty) lines during a synchronized update, also track scrolled lines. This means they can be scrolled as one instead of forcing each line to be redrawn - much less expensive for a very common operation. From Ben Maurer in GitHub issue 5611.
2026-09-24 12:27:53 +00:00
nicm 7fc7f1bf69 As well as tracking modified (dirty) lines during a synchronized update,
also track scrolled lines. This means they can be scrolled as one
instead of forcing each line to be redrawn - much less expensive for a
very common operation. From Ben Maurer in GitHub issue 5611.
2026-09-24 12:27:51 +00:00
Nicholas Marriott 29e6b80286 Tests for SU scrolling. 2026-09-24 12:20:02 +01:00
Nicholas Marriott 6973c6b221 Damage rectanges. 2026-09-24 11:49:24 +01:00
tmux update bot a4a2501b2f Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add margins for WezTerm and ghostty, from Michael Grant.
2026-09-24 09:44:56 +00:00
nicm aa4123ab58 Add margins for WezTerm and ghostty, from Michael Grant. 2026-09-24 09:44:54 +00:00
Nicholas Marriott 6bab150c58 Fix test. 2026-09-24 09:15:21 +01:00
tmux update bot 72e83b000b Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Note about default-shell, from Meriel Sartha Mittelbach.
2026-09-23 16:48:10 +00:00
nicm a4c051f27d Note about default-shell, from Meriel Sartha Mittelbach. 2026-09-23 16:48:08 +00:00
Nicholas Marriott 56373a08ee Merge pull request #5628 from arthurscchan/new-fuzzer
OSS-Fuzz: Add new fuzzer targets layout processing
2026-09-23 12:03:00 +01:00
Arthur Chan be75a67b1f OSS-Fuzz: Add new fuzzer targets layout processing
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-09-22 16:45:24 +01:00
75 changed files with 2717 additions and 8446 deletions
+34 -5
View File
@@ -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. A modal pane is now used for
the editor in buffer mode;
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;
- new-pane -A creates a floating pane which remains above a zoomed pane;
modal panes do this by default;
@@ -39,6 +39,9 @@ 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
@@ -115,6 +118,12 @@ 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).
@@ -189,10 +198,23 @@ 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).
client that stops reading cannot consume unlimited memory (issue 5565), and
fix a collision between the output discard and bracketed paste flags (issue
5622).
* Fix grouped sessions sometimes being left as unusable command targets while
they are being killed (Bryce Miller, issue 5180).
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.
* 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.
@@ -203,11 +225,18 @@ 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).
5098), and misplaced wrapped lines during partial redraws (Ben Maurer, issue
5625).
* 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
+14 -6
View File
@@ -5,8 +5,7 @@ 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 \
tools/generate-image-diacritics.sh
osdep-*.c mdoc2man.awk tmux.1
dist_EXTRA_tmux_SOURCES = compat/*.[ch]
# Preprocessor flags.
@@ -16,6 +15,9 @@ 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)
@@ -247,9 +249,9 @@ if HAVE_UTF8PROC
nodist_tmux_SOURCES += compat/utf8proc.c
endif
# Enable image support.
if ENABLE_IMAGES
dist_tmux_SOURCES += image.c image-sixel.c image-kitty.c image-fallback.c
# Enable sixel support.
if ENABLE_SIXEL
dist_tmux_SOURCES += image.c image-sixel.c
endif
# Add bits for fuzzing if enabled.
@@ -258,7 +260,9 @@ check_PROGRAMS = \
fuzz/input-fuzzer \
fuzz/cmd-parse-fuzzer \
fuzz/format-fuzzer \
fuzz/style-fuzzer
fuzz/style-fuzzer \
fuzz/layout-fuzzer \
fuzz/json-fuzzer
fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_input_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_cmd_parse_fuzzer_LDFLAGS = $(FUZZING_LIBS)
@@ -267,6 +271,10 @@ 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.
+6 -8
View File
@@ -307,8 +307,7 @@ 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;
int old_xoff, old_yoff, old_sx, old_sy;
int y, ly, x, lx, oxoff, oyoff, osx, osy;
wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) {
@@ -330,17 +329,16 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
ly = m->statusat - 1;
if (x != lx || y != ly) {
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = wp->sx;
old_sy = wp->sy;
oxoff = wp->xoff;
oyoff = wp->yoff;
osx = wp->sx;
osy = wp->sy;
lc->g.xoff += x - lx;
lc->g.yoff += y - ly;
layout_fix_panes(w, NULL);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
server_redraw_window_borders(w);
}
}
+3 -7
View File
@@ -237,9 +237,8 @@ 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;
int new_xoff, new_yoff, resizes = 0;
int old_xoff, old_yoff, old_sx, old_sy;
int left, right, resizes = 0;
int new_xoff, new_yoff, old_xoff, old_yoff;
wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) {
@@ -252,8 +251,6 @@ 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) &&
@@ -353,8 +350,7 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
}
if (resizes != 0) {
layout_fix_panes(w, NULL);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
window_redraw_floating_pane(wp, old_xoff, old_yoff, sx, sy);
server_redraw_window_borders(w);
}
}
+6 -7
View File
@@ -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 old_xoff, old_yoff, old_sx, old_sy;
int oxoff, oyoff, osx, osy;
if (c->tty.mouse_last_pane == -1)
return;
@@ -418,15 +418,14 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
if (sy < PANE_MINIMUM)
sy = PANE_MINIMUM;
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = wp->sx;
old_sy = wp->sy;
oxoff = wp->xoff;
oyoff = wp->yoff;
osx = wp->sx;
osy = wp->sy;
layout_set_size(lc, sx, sy, xoff, yoff);
layout_fix_panes(w, NULL);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
server_redraw_window_borders(w);
}
+18 -27
View File
@@ -111,6 +111,17 @@ 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)
@@ -530,35 +541,15 @@ if test "x$enable_cgroups" = xyes; then
fi
fi
# 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.
# Enable sixel support.
AC_ARG_ENABLE(
sixel,
AS_HELP_STRING(--enable-sixel, deprecated alias for --enable-images)
AS_HELP_STRING(--enable-sixel, enable sixel images)
)
if test "x$enable_sixel" = xyes; then
AC_MSG_WARN([--enable-sixel is deprecated, use --enable-images instead to enable.])
enable_images=yes
AC_DEFINE(ENABLE_SIXEL)
fi
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])
AM_CONDITIONAL(ENABLE_SIXEL, [test "x$enable_sixel" = xyes])
# Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well.
AC_MSG_CHECKING(for b64_ntop)
@@ -1157,10 +1148,10 @@ if test "x$enable_asan" = xyes; then
else
AC_MSG_NOTICE([ASAN: off])
fi
if test "x$enable_images" = xyes; then
AC_MSG_NOTICE([images: on])
if test "x$enable_sixel" = xyes; then
AC_MSG_NOTICE([SIXEL: on])
else
AC_MSG_NOTICE([images: off])
AC_MSG_NOTICE([SIXEL: off])
fi
if test "x$enable_debug" = xyes; then
AC_MSG_NOTICE([debug: on])
-14
View File
@@ -3078,17 +3078,6 @@ 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)
@@ -3761,9 +3750,6 @@ 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
},
+88
View File
@@ -0,0 +1,88 @@
/*
* 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;
}
+37
View File
@@ -0,0 +1,37 @@
# 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"
+2
View File
@@ -0,0 +1,2 @@
[libfuzzer]
max_len = 4096
+111
View File
@@ -0,0 +1,111 @@
/*
* 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;
}
+17
View File
@@ -0,0 +1,17 @@
# 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["
"@"
+2
View File
@@ -0,0 +1,2 @@
[libfuzzer]
max_len = 1024
+4 -78
View File
@@ -86,9 +86,6 @@ 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
@@ -356,9 +353,6 @@ 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);
@@ -403,9 +397,6 @@ 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);
}
@@ -738,9 +729,6 @@ 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);
@@ -776,10 +764,6 @@ 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)
@@ -791,18 +775,8 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
return;
for (yy = py; yy < py + ny; 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_free_line(gd, yy);
grid_empty_line(gd, yy, bg);
#ifdef ENABLE_IMAGES
gl->images = images;
#endif
}
if (py != 0)
gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED;
@@ -867,9 +841,6 @@ 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)
@@ -1242,6 +1213,7 @@ 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);
@@ -1308,18 +1280,11 @@ 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++) {
@@ -1327,9 +1292,6 @@ 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);
@@ -1349,9 +1311,6 @@ 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. */
@@ -1362,30 +1321,6 @@ 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)
@@ -1448,10 +1383,6 @@ 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;
@@ -1512,7 +1443,8 @@ 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++) {
grid_free_line(gd, i);
free(gd->linedata[i].celldata);
free(gd->linedata[i].extddata);
grid_reflow_dead(&gd->linedata[i]);
}
@@ -1614,12 +1546,6 @@ 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
-464
View File
@@ -1,464 +0,0 @@
/* $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
View File
File diff suppressed because it is too large Load Diff
+109 -1128
View File
File diff suppressed because it is too large Load Diff
+149 -1662
View File
File diff suppressed because it is too large Load Diff
+6 -99
View File
@@ -144,9 +144,6 @@ struct input_ctx {
*/
struct evbuffer *since_ground;
struct event ground_timer;
#ifdef ENABLE_IMAGES
void *kitty_state;
#endif
};
/* Helper functions. */
@@ -185,9 +182,6 @@ 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 *);
@@ -920,9 +914,6 @@ 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);
@@ -945,7 +936,6 @@ 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);
}
@@ -1074,7 +1064,6 @@ 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);
@@ -1094,7 +1083,6 @@ 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);
}
@@ -1594,7 +1582,7 @@ input_csi_dispatch(struct input_ctx *ictx)
case -1:
break;
case 0:
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
input_reply(ictx, 1, "\033[?1;2;4c");
#else
input_reply(ictx, 1, "\033[?1;2c");
@@ -2112,7 +2100,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx)
static void
input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx)
{
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
int n, m, o;
if (ictx->param_list_len > 3)
@@ -2637,10 +2625,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_IMAGES
#ifdef ENABLE_SIXEL
struct window *w;
struct sixel_image *si;
int p1, p2;
int p2;
#endif
if (wp == NULL)
@@ -2653,18 +2641,15 @@ input_dcs_dispatch(struct input_ctx *ictx)
return (0);
}
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
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, p1, p2, w->xpixel, w->ypixel);
si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel);
if (si != NULL)
screen_write_sixelimage(sctx, si, ictx->cell.cell.bg);
}
@@ -2803,57 +2788,6 @@ 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)
{
@@ -2864,13 +2798,6 @@ 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)) {
@@ -2931,12 +2858,6 @@ 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;
@@ -2964,20 +2885,6 @@ 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
View File
@@ -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 (0); /* no tiled cell anywhere: empty v1 body */
return (-1);
lccompat = layout_custom_create_compat(lcroot);
result = layout_append_v1(lccompat, ls);
layout_custom_free_compat(lccompat);
+1 -1
View File
@@ -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)
+18 -13
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: mode-tree.c,v 1.102 2026/09/21 12:14:32 nicm Exp $ */
/* $OpenBSD: mode-tree.c,v 1.103 2026/09/24 18:59:00 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -277,11 +277,22 @@ 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 reset the offset to the
* last visible line.
* If the current line would now be off screen move the offset so it is
* visible.
*/
if (mtd->current > mtd->height - 1)
if (mtd->current < mtd->offset)
mtd->offset = mtd->current;
else if (mtd->current > mtd->offset + mtd->height - 1)
mtd->offset = mtd->current - mtd->height + 1;
}
@@ -501,20 +512,14 @@ mode_tree_set_current(struct mode_tree_data *mtd, uint64_t tag)
if (mode_tree_get_tag(mtd, tag, &found)) {
mtd->current = found;
if (mtd->current > mtd->height - 1)
mtd->offset = mtd->current - mtd->height + 1;
else
mtd->offset = 0;
mode_tree_check_selected(mtd);
return (1);
}
if (mtd->current >= mtd->line_size) {
if (mtd->line_size == 0)
return (0);
mtd->current = mtd->line_size - 1;
if (mtd->current > mtd->height - 1)
mtd->offset = mtd->current - mtd->height + 1;
else
mtd->offset = 0;
mode_tree_check_selected(mtd);
}
return (0);
}
@@ -683,13 +688,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);
}
-1
View File
@@ -552,7 +552,6 @@ 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 = ",",
+5 -3
View File
@@ -153,11 +153,13 @@ 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 -g @after fast ||
$TMUX display-panes -Nd 500 -t "$p0" \; \
set -gF -t "$p0" @after '#{pane_mode}' ||
fail "display-panes immediate command failed"
wait_option @after fast
wait_format "$p0" '#{pane_mode}' 'panes-mode'
wait_option @after 'panes-mode'
wait_format "$p0" '#{pane_in_mode}' '0'
# Existing zoom is restored on exit.
+135
View File
@@ -0,0 +1,135 @@
#!/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,17 +1,6 @@
#!/bin/sh
# 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.
# Each drag motion must draw the floating pane content once.
PATH=/bin:/usr/bin
TERM=screen
@@ -22,7 +11,7 @@ export PATH TERM LC_ALL
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Ldoublecomp-inner-$$ -f/dev/null"
INNER="$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null"
fail()
@@ -47,14 +36,16 @@ mouse()
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec 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 \
'sleep 100') || exit 1
"printf 'DRAGMARK'; exec sleep 100") || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER new-session -d -s outer -x 40 -y 15 '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
@@ -68,13 +59,18 @@ YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# 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.
# Begin capture after focus changes from the initial mouse press have settled.
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))
@@ -83,18 +79,10 @@ mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$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"
[ "$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"
exit 0
@@ -137,4 +137,23 @@ 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,16 +1,6 @@
#!/bin/sh
# 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.
# Synchronized drag output must not cause a full redraw of untouched rows.
PATH=/bin:/usr/bin
TERM=screen
@@ -21,7 +11,7 @@ export PATH TERM LC_ALL
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Lsyncdefer-inner-$$ -f/dev/null"
INNER="$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null"
fail()
@@ -46,18 +36,20 @@ mouse()
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec 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 \
'sleep 100') || exit 1
"printf 'DRAGMARK'; exec sleep 100") || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER new-session -d -s outer -x 40 -y 15 '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 set-option -as terminal-features ',screen-256color:sync' || exit 1
$INNER 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
@@ -68,13 +60,18 @@ YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# 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.
# Begin capture after focus changes from the initial mouse press have settled.
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
while [ $i -lt 6 ]; do
steps=6
while [ "$i" -lt "$steps" ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
@@ -83,13 +80,14 @@ mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$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)"
[ "$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
exit 0
@@ -1,20 +1,7 @@
#!/bin/sh
# 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.
# Returning from the alternate screen must redraw the visible part of a
# floating pane clipped at the left edge, the top edge, or both.
PATH=/bin:/usr/bin
TERM=screen
@@ -57,28 +44,24 @@ 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;
print "\e[1;1H", 'A' x 15;
sleep 2;
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[?1049h";
print "\e[1;1H", 'B' x 15;
sleep 2;
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[?1049l";
sleep 100;
PERL
@@ -87,15 +70,6 @@ $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
@@ -104,8 +78,25 @@ $OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_outer_has AAAAA
wait_outer_has BBBBB
wait_visible_restored
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
exit 0
+113 -79
View File
@@ -1,31 +1,7 @@
#!/bin/sh
# 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.
# Damage redraws must show each client's own pane status, even though the
# cached status screen is shared between clients.
PATH=/bin:/usr/bin
TERM=screen
@@ -35,83 +11,141 @@ export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
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"
INNER="$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lstatuscc-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER1 kill-server 2>/dev/null
$OUTER2 kill-server 2>/dev/null
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
BASEEMITTER=$DIR/base-emitter.pl
cat >"$BASEEMITTER" <<'PERL'
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;
my $line = <STDIN>;
print "\e]4;1;rgb:11/22/33\e\\";
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";
}
sleep 100;
PERL
$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 \
$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
$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
$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
sleep 0.5
NAME1=$($INNER list-clients -F '#{client_name}') || exit 1
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
$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"
# 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
exit 0
-151
View File
@@ -1,151 +0,0 @@
#!/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
-71
View File
@@ -1,71 +0,0 @@
#!/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
-101
View File
@@ -1,101 +0,0 @@
#!/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
-110
View File
@@ -1,110 +0,0 @@
#!/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
-114
View File
@@ -1,114 +0,0 @@
#!/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
-107
View File
@@ -1,107 +0,0 @@
#!/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
-142
View File
@@ -1,142 +0,0 @@
#!/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
-100
View File
@@ -1,100 +0,0 @@
#!/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
-96
View File
@@ -1,96 +0,0 @@
#!/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
-362
View File
@@ -1,362 +0,0 @@
#!/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
+1 -5
View File
@@ -60,11 +60,7 @@ query_timeout()
query "dsr-ok" '^[[0n' '\033[5n' 4 ''
query "dsr-cursor" '^[[1;1R' '\033[6n' 6 ''
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-primary" '^[[?1;2c' '\033[c' 7 ''
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'
+168
View File
@@ -0,0 +1,168 @@
#!/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
+126
View File
@@ -0,0 +1,126 @@
#!/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
+8
View File
@@ -131,6 +131,14 @@ 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'
+113
View File
@@ -0,0 +1,113 @@
#!/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
+140
View File
@@ -0,0 +1,140 @@
#!/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
+112
View File
@@ -0,0 +1,112 @@
#!/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
+49
View File
@@ -0,0 +1,49 @@
#!/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
+123
View File
@@ -0,0 +1,123 @@
#!/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
+150
View File
@@ -0,0 +1,150 @@
#!/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
+49 -73
View File
@@ -1,23 +1,7 @@
#!/bin/sh
# 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.
# With left/right margins, a pane narrowed by a scrollbar can scroll without
# retransmitting its existing rows. Both paths must produce the same screen.
PATH=/bin:/usr/bin
TERM=screen
@@ -27,8 +11,7 @@ export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Lmarginsscrollbar-inner-$$ -f/dev/null"
INNER="$TEST_TMUX -Lmarginsscrollbar-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lmarginsscrollbar-outer-$$ -f/dev/null"
fail()
@@ -41,77 +24,70 @@ cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
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"
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
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
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 ',*:margins' || exit 1
if [ "$margins" = on ]; then
$INNER set -as terminal-features ',screen-256color:margins' || exit 1
fi
$OUTER new-session -d -x 40 -y 6 || exit 1
OUTERPANE=$($OUTER list-panes -F '#{pane_id}') || exit 1
$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 send-keys -t "$OUTERPANE" -l "$INNER attach -t inner" || exit 1
$OUTER send-keys -t "$OUTERPANE" Enter || exit 1
sleep 1
wait_for_client
$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"
i=0
while [ "$i" -lt 8 ]; do
$INNER send-keys -t inner Enter || exit 1
sleep 0.2
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
sleep 0.1
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")
[ "$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"
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 kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
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"
done
exit 0
+26
View File
@@ -0,0 +1,26 @@
#!/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
+3 -16
View File
@@ -152,18 +152,7 @@ clients_calculate_size(int type, int current, struct client *c,
*sx = UINT_MAX;
*sy = UINT_MAX;
}
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;
*xpixel = *ypixel = 0;
/*
* For latest, count the number of clients with this window. We only
@@ -397,12 +386,10 @@ 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 &&
w->new_xpixel == xpixel && w->new_ypixel == ypixel)
if (!now && changed && w->new_sx == sx && w->new_sy == sy)
changed = 0;
} else {
if (!now && changed && w->sx == sx && w->sy == sy &&
w->xpixel == xpixel && w->ypixel == ypixel)
if (!now && changed && w->sx == sx && w->sy == sy)
changed = 0;
}
+134 -662
View File
File diff suppressed because it is too large Load Diff
+261 -117
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.292 2026/09/21 12:14:32 nicm Exp $ */
/* $OpenBSD: screen-write.c,v 1.293 2026/09/24 11:19:39 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -34,15 +34,13 @@ 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_flush_dirty(struct window_pane *);
static void screen_write_sync_flush_dirty(struct window_pane *);
static void screen_write_sync_apply_scroll(struct screen_write_ctx *,
struct tty_ctx *);
struct screen_write_citem {
u_int x;
@@ -124,18 +122,7 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy)
evtimer_add(&w->offset_timer, &tv);
}
/*
* 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.
*/
/* Redraw lines. */
static void
screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny)
{
@@ -153,10 +140,8 @@ 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)
return;
redraw_damage_window(wp->window, (u_int)x0, (u_int)y0,
(u_int)(x1 - x0), (u_int)(y1 - y0));
if (x1 > x0 && y1 > y0)
redraw_damage_window(wp->window, x0, y0, x1 - x0, y1 - y0);
}
/* Update context for client. */
@@ -244,6 +229,27 @@ 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)
@@ -257,21 +263,9 @@ 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;
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;
}
bs = screen_write_sync_allocate_dirty(wp, sy);
bit_nset(bs, y, y + ny - 1);
}
return (0);
@@ -395,19 +389,6 @@ 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,
@@ -1074,7 +1055,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_flush_dirty(wp);
screen_write_sync_flush_dirty(wp);
}
}
@@ -1106,7 +1087,7 @@ screen_write_stop_sync(struct window_pane *wp)
evtimer_del(&wp->sync_timer);
wp->base.mode &= ~MODE_SYNC;
screen_write_flush_dirty(wp);
screen_write_sync_flush_dirty(wp);
log_debug("%s: %%%u stopped sync mode", __func__, wp->id);
}
@@ -1314,7 +1295,7 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
/* Redraw dirty lines. */
static void
screen_write_flush_dirty(struct window_pane *wp)
screen_write_sync_flush_dirty(struct window_pane *wp)
{
struct screen_write_ctx ctx;
struct tty_ctx ttyctx;
@@ -1327,29 +1308,143 @@ screen_write_flush_dirty(struct window_pane *wp)
screen_write_start_pane(&ctx, wp, s);
screen_write_initctx(&ctx, &ttyctx, 1, 1);
for (y = 0; y < sy; y++) {
if (bit_test(wp->sync_dirty, y)) {
screen_write_redraw_line(&ctx, &ttyctx, y);
lines++;
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++;
}
}
}
log_debug("%s: %%%u had %u dirty lines", __func__, wp->id, lines);
screen_write_stop(&ctx);
screen_write_clear_dirty(wp);
screen_write_sync_clear_dirty(wp);
}
/* Clear any dirty lines. */
/* Clear pending synchronized output. */
void
screen_write_clear_dirty(struct window_pane *wp)
screen_write_sync_clear_dirty(struct window_pane *wp)
{
if (wp != NULL && wp->sync_dirty != NULL) {
if (wp != 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)
@@ -1373,11 +1468,9 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx)
memcpy(&gc, &grid_default_cell, sizeof gc);
utf8_set(&gc.data, 'E');
#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));
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
for (yy = 0; yy < screen_size_y(s); yy++) {
@@ -1422,8 +1515,9 @@ 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_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1462,8 +1556,9 @@ 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_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1502,8 +1597,9 @@ 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_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1536,8 +1632,9 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0)
ny = 1;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
if (s->cy < s->rupper || s->cy > s->rlower) {
@@ -1603,8 +1700,9 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0)
ny = 1;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
if (s->cy < s->rupper || s->cy > s->rlower) {
@@ -1675,8 +1773,9 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg)
if (gl->cellsize == 0 && COLOUR_DEFAULT(bg))
return;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
flags = gl->flags & GRID_LINE_OSC133_FLAGS;
@@ -1713,8 +1812,9 @@ 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_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg);
@@ -1739,8 +1839,9 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg)
return;
}
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
if (s->cx > sx - 1)
@@ -1792,8 +1893,9 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
return;
}
#ifdef ENABLE_IMAGES
image_redraw_scroll(ctx, 1);
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg);
@@ -1843,6 +1945,9 @@ 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);
@@ -1863,12 +1968,13 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
return;
}
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
if (rlower == screen_size_y(s) - 1)
image_redraw_scroll(ctx, 1);
redraw = image_scroll_up(s, 1);
else
image_redraw_area(ctx, 0, rupper, screen_size_x(s),
rlower - rupper);
redraw = image_check_line(s, rupper, rlower - rupper);
if (redraw && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg);
@@ -1894,8 +2000,9 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg)
ctx->bg = bg;
}
#ifdef ENABLE_IMAGES
image_redraw_scroll(ctx, lines);
#ifdef ENABLE_SIXEL
if (image_scroll_up(s, lines) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
for (i = 0; i < lines; i++) {
@@ -1922,8 +2029,9 @@ 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_IMAGES
image_redraw_scroll(ctx, lines);
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
for (i = 0; i < lines; i++)
@@ -1962,8 +2070,9 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2042,8 +2151,9 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, 0, screen_size_x(s), s->cy + 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, 0, s->cy - 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2114,8 +2224,9 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2453,10 +2564,8 @@ 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_should_draw_lines(ctx, s->rupper,
s->rlower + 1 - s->rupper);
}
if (ctx->scrolled != 0)
screen_write_sync_scroll_dirty(ctx);
for (y = 0; y < screen_size_y(s); y++) {
cl = &s->write_list[y];
if (!TAILQ_EMPTY(&cl->items))
@@ -2618,8 +2727,9 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
}
}
#ifdef ENABLE_IMAGES
screen_write_image_damage(ctx, s->cx, s->cy, ci->used, 1);
#ifdef ENABLE_SIXEL
if (image_check_area(s, s->cx, s->cy, ci->used, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x,
@@ -2749,11 +2859,6 @@ 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) {
@@ -2981,9 +3086,6 @@ 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);
@@ -3119,21 +3221,63 @@ screen_write_rawstring(struct screen_write_ctx *ctx, u_char *str, u_int len,
tty_write(tty_cmd_rawstring, &ttyctx);
}
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
/* Write a SIXEL image. */
void
screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si,
u_int bg)
{
struct image *im;
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;
im = sixel_to_image(si);
if (im == NULL) {
sixel_free(si);
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);
sixel_free(si);
if (new == NULL)
return;
sixel_size_in_cells(new, &x, &y);
si = new;
}
image_write_sixel(ctx, im, bg);
image_free(image_get_id(im));
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);
}
#endif
+50
View File
@@ -93,6 +93,11 @@ 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;
@@ -126,6 +131,11 @@ 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);
}
@@ -144,6 +154,10 @@ 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);
@@ -160,6 +174,17 @@ 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. */
@@ -352,6 +377,10 @@ 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);
@@ -663,6 +692,10 @@ 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);
@@ -676,6 +709,12 @@ 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;
@@ -689,6 +728,10 @@ 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.
@@ -731,6 +774,13 @@ 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)
+17 -81
View File
@@ -199,7 +199,6 @@ 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);
@@ -359,30 +358,10 @@ server_client_set_session(struct client *c, struct session *s)
server_client_fire_session_changed(c, old);
/*
* 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.
* Redraw if the session or displayed window changed. Use the
* cached scene because the session's current window is already set.
*/
if (old == NULL || old != s ||
!redraw_client_has_window(c, s->curw->window))
if (old != s || !redraw_client_has_window(c, s->curw->window))
server_redraw_client(c);
}
@@ -434,7 +413,6 @@ server_client_lost(struct client *c)
status_free(c);
input_cancel_requests(c);
redraw_free_pending_damage(c);
free(c->title);
free(c->path);
@@ -1377,18 +1355,8 @@ server_client_key_callback(struct cmdq_item *item, void *data)
m->key = key;
/*
* 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.
* Synchronize direct drag output with the later damage redraw
* before invoking the drag callback.
*/
if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) {
tty_sync_start(&c->tty);
@@ -1818,15 +1786,8 @@ server_client_loop(void)
}
/*
* 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.
* Clear window redraw state after processing all clients. Deferred
* redraws are preserved in client flags.
*/
RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry) {
@@ -2386,8 +2347,6 @@ 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);
@@ -2404,6 +2363,7 @@ 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;
@@ -2429,29 +2389,12 @@ server_client_check_redraw(struct client *c)
return;
}
/*
* 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.
*/
/* Ignore output queued within the current synchronized frame. */
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);
@@ -2463,8 +2406,10 @@ server_client_check_redraw(struct client *c)
log_debug("redraw timer started");
evtimer_add(&ev, &tv);
}
if (!TAILQ_EMPTY(&w->damage))
redraw_defer_damage(c);
if (damaged) {
c->flags |= CLIENT_REDRAWWINDOW;
return;
}
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW;
@@ -2499,17 +2444,8 @@ server_client_check_redraw(struct client *c)
}
}
/*
* 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))
/* Draw damage here if no client redraw flags will handle it. */
if (damaged && (c->flags & CLIENT_ALLREDRAWFLAGS) == 0)
redraw_client_damage(c);
}
+1 -2
View File
@@ -30,8 +30,7 @@
/* 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,
+10 -12
View File
@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1172 2026/09/22 06:58:05 nicm Exp $
.\" $OpenBSD: tmux.1,v 1.1173 2026/09/23 12:37:50 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 22 2026 $
.Dd $Mdocdate: September 23 2026 $
.Dt TMUX 1
.Os
.Sh NAME
@@ -994,13 +994,19 @@ 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
and
.Ic respawn\-window ,
.Ic respawn\-pane
and
.Ic display\-popup
commands allow
.Ar shell\-command
to be given as multiple arguments and executed directly (without
@@ -5100,20 +5106,12 @@ 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
+45 -232
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1447 2026/09/22 14:10:26 nicm Exp $ */
/* $OpenBSD: tmux.h,v 1.1448 2026/09/24 11:19:39 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -77,12 +77,7 @@ struct screen_write_cline;
struct screen_write_ctx;
struct session;
#ifdef ENABLE_IMAGES
struct image;
struct image_backend;
struct image_rect;
struct image_line;
struct image_store;
#ifdef ENABLE_SIXEL
struct sixel_image;
#endif
@@ -868,7 +863,7 @@ struct colour_palette {
struct grid_cell {
struct utf8_data data;
u_short attr;
u_short flags;
u_char flags;
int fg;
int bg;
int us;
@@ -879,7 +874,7 @@ struct grid_cell {
struct grid_extd_entry {
utf8_char data;
u_short attr;
u_short flags;
u_char flags;
int fg;
int bg;
int us;
@@ -897,7 +892,7 @@ struct grid_cell_entry {
u_char data;
} data;
};
u_short flags;
u_char flags;
} __packed;
/* OSC 133 data for a grid line. */
@@ -921,9 +916,6 @@ 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. */
@@ -943,9 +935,6 @@ struct grid {
u_int scroll_generation;
struct grid_line *linedata;
#ifdef ENABLE_IMAGES
struct image_store *images;
#endif
};
/* Virtual cursor in a grid. */
@@ -1037,65 +1026,24 @@ struct style {
u_int link;
};
#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
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];
};
#ifdef ENABLE_SIXEL
/* Image. */
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;
struct screen *s;
struct sixel_image *data;
char *fallback;
u_int px;
u_int py;
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;
RB_ENTRY(image) entry;
struct images *list;
TAILQ_ENTRY (image) entry;
TAILQ_ENTRY (image) all_entry;
};
RB_HEAD(images, image);
#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024)
TAILQ_HEAD(images, image);
#endif
/* Cursor style. */
@@ -1154,6 +1102,11 @@ 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;
@@ -1171,7 +1124,6 @@ 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;
@@ -1396,6 +1348,10 @@ 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;
@@ -1451,7 +1407,7 @@ struct window_pane {
struct screen base;
struct screen status_screen;
u_int status_serial;
u_int status_generation;
TAILQ_HEAD(, window_mode_entry) modes;
@@ -1528,10 +1484,6 @@ 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;
@@ -1800,13 +1752,7 @@ struct tty_term {
#define TERM_VT100LIKE 0x20
#define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80
#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
#define TERM_NOREPLACE 0x100
int flags;
LIST_ENTRY(tty_term) entry;
@@ -1864,12 +1810,6 @@ 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;
@@ -1916,13 +1856,6 @@ 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. */
@@ -1958,7 +1891,7 @@ struct tty_ctx {
size_t size;
} sel;
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
struct image *image;
#endif
};
@@ -2319,23 +2252,6 @@ 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;
@@ -3074,6 +2990,10 @@ 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 *);
@@ -3115,7 +3035,6 @@ 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);
@@ -3679,7 +3598,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_clear_dirty(struct window_pane *);
void screen_write_sync_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);
@@ -3713,7 +3632,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_IMAGES
#ifdef ENABLE_SIXEL
void screen_write_sixelimage(struct screen_write_ctx *,
struct sixel_image *, u_int);
#endif
@@ -3731,15 +3650,8 @@ 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 */
@@ -3913,9 +3825,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 *);
@@ -4304,115 +4216,17 @@ void spawn_editor_finish(struct window_pane *);
/* regsub.c */
char *regsub(const char *, const char *, const char *, int);
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
/* image.c */
#define IMAGE_BACKEND_GRAPHICAL 0x1
#define IMAGE_BACKEND_SCROLLS 0x2
#define IMAGE_BACKEND_CLIPPED 0x4
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);
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
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);
struct sixel_image *sixel_parse(const char *, size_t, 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 *);
@@ -4421,7 +4235,6 @@ 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 */
-51
View File
@@ -1,51 +0,0 @@
#!/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"
-104
View File
@@ -1,104 +0,0 @@
#!/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
-109
View File
@@ -1,109 +0,0 @@
#!/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
-1
View File
@@ -1 +0,0 @@
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-\
-44
View File
@@ -1,44 +0,0 @@
#!/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"
+1 -66
View File
@@ -64,65 +64,9 @@ 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 */
#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
{ '~', "\302\267" } /* bullet */
};
#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;
@@ -295,15 +239,6 @@ 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);
+5 -24
View File
@@ -122,10 +122,6 @@ 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;
@@ -255,35 +251,20 @@ 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(gcp, &last, nx - i);
empty = tty_draw_line_get_empty(&gc, &last,
nx - i);
if (empty != 0)
;
gcp = &gc;
else {
/* Update for codeset if needed. */
gcp = tty_check_codeset(tty, gcp);
gcp = tty_check_codeset(tty, &gc);
/* And for selection. */
if (gcp->flags & GRID_FLAG_SELECTED) {
+3 -72
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: tty-features.c,v 1.45 2026/09/22 14:10:26 nicm Exp $ */
/* $OpenBSD: tty-features.c,v 1.46 2026/09/24 08:16:13 nicm Exp $ */
/*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -236,11 +236,9 @@ 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[] = {
TTY_FEATURE_SYNC,
"Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;",
NULL
};
static const struct tty_feature tty_feature_sync = {
@@ -371,50 +369,6 @@ 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\\\\",
@@ -445,12 +399,6 @@ 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,
@@ -689,9 +637,7 @@ tty_default_features(struct client *c, const char *name, u_int version)
"extkeys,"
"focus,"
"hyperlinks,"
"imagescroll,"
"margins,"
"sixel,"
"usstyle"
},
{ .name = "ghostty",
@@ -700,28 +646,13 @@ tty_default_features(struct client *c, const char *name, u_int version)
"cstyle,"
"extkeys,"
"focus,"
"imagescroll,"
"margins,"
"overline,"
"hyperlinks,"
"osc7,"
"sync,"
"usstyle,"
"progressbar,"
"kitty"
},
{ .name = "kitty",
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
"ccolour,"
"cstyle,"
"extkeys,"
"focus,"
"hyperlinks,"
"imagescroll,"
"osc7,"
"sync,"
"usstyle,"
"kitty"
"progressbar"
},
{ .name = "Rio",
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
-1
View File
@@ -729,7 +729,6 @@ 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;
-1
View File
@@ -531,7 +531,6 @@ 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)
{
+124 -72
View File
@@ -67,6 +67,11 @@ 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) \
@@ -157,21 +162,10 @@ 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
@@ -289,10 +283,6 @@ 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,
@@ -537,9 +527,6 @@ 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);
@@ -556,25 +543,15 @@ 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);
@@ -587,22 +564,7 @@ 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);
@@ -1493,6 +1455,59 @@ 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)
@@ -1568,6 +1583,19 @@ 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)
{
@@ -1702,17 +1730,10 @@ 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);
@@ -1737,17 +1758,10 @@ 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);
@@ -1782,17 +1796,10 @@ 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);
@@ -1827,17 +1834,10 @@ 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,6 +2029,58 @@ 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)
{
-4
View File
@@ -374,10 +374,6 @@ 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
View File
@@ -73,10 +73,6 @@ 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);
@@ -283,10 +279,6 @@ 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;
@@ -644,9 +636,6 @@ 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);
@@ -1209,9 +1198,6 @@ 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);
@@ -5238,34 +5224,6 @@ 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) {
@@ -5514,29 +5472,6 @@ 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') {
@@ -5597,55 +5532,6 @@ 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)
{
@@ -5655,10 +5541,6 @@ 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++)
@@ -7047,13 +6929,6 @@ 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) {
@@ -7125,13 +7000,6 @@ 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) {
+18 -91
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: window.c,v 1.377 2026/09/21 10:33:16 nicm Exp $ */
/* $OpenBSD: window.c,v 1.378 2026/09/24 11:19:39 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_clear_dirty(wp);
screen_write_sync_clear_dirty(wp);
if (wp->fd != -1) {
#ifdef HAVE_UTEMPTER
@@ -1692,10 +1692,6 @@ 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)
@@ -2946,31 +2942,21 @@ window_pane_is_floating_with_hidden(struct window_pane *wp)
return (1);
}
/*
* 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.
*/
/* Report damage for a floating pane, including its border and scrollbar. */
static void
window_pane_damage_floating(struct window *w, struct window_pane *wp,
int xoff, int yoff, int sx, int sy)
window_damage_floating_pane(struct window_pane *wp, int xoff, int yoff,
int sx, int sy)
{
int x0, x1, y0, y1, sb_left = 0, sb_right = 0;
struct window *w = wp->window;
int x0, x1, y0, y1, sb_left = 0, sb_right = 0;
struct style *sb_sy = &wp->scrollbar_style;
if (window_pane_scrollbar_reserve(wp)) {
if (w->sb_pos == PANE_SCROLLBARS_LEFT)
sb_left = wp->scrollbar_style.width +
wp->scrollbar_style.pad;
sb_left = sb_sy->width + sb_sy->pad;
else
sb_right = wp->scrollbar_style.width +
wp->scrollbar_style.pad;
sb_right = sb_sy->width + sb_sy->pad;
}
x0 = xoff - 1 - sb_left;
x1 = xoff + sx + sb_right;
y0 = yoff - 1;
@@ -2979,75 +2965,16 @@ window_pane_damage_floating(struct window *w, struct window_pane *wp,
x0 = 0;
if (y0 < 0)
y0 = 0;
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);
if (x1 >= x0 && y1 >= y0)
redraw_damage_window(w, x0, y0, x1 - x0 + 1U, y1 - y0 + 1U);
}
/*
* 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)
{
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).
*/
/* Report damage for a floating pane's old and new areas. */
void
window_pane_redraw_floating(struct window *w, struct window_pane *wp,
int old_xoff, int old_yoff, int old_sx, int old_sy)
window_redraw_floating_pane(struct window_pane *wp, int oxoff, int oyoff,
int osx, int osy)
{
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);
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);
}