mirror of
https://github.com/tmux/tmux.git
synced 2026-10-16 16:44:30 +00:00
Kitty image input path.
This commit is contained in:
5
.github/workflows/regress.yml
vendored
5
.github/workflows/regress.yml
vendored
@@ -53,8 +53,10 @@ jobs:
|
|||||||
build-essential \
|
build-essential \
|
||||||
libevent-dev \
|
libevent-dev \
|
||||||
libncurses-dev \
|
libncurses-dev \
|
||||||
|
libpng-dev \
|
||||||
libutf8proc-dev \
|
libutf8proc-dev \
|
||||||
pkg-config
|
pkg-config \
|
||||||
|
zlib1g-dev
|
||||||
|
|
||||||
- name: dependencies
|
- name: dependencies
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
@@ -64,6 +66,7 @@ jobs:
|
|||||||
automake \
|
automake \
|
||||||
bison \
|
bison \
|
||||||
libevent \
|
libevent \
|
||||||
|
libpng \
|
||||||
jemalloc \
|
jemalloc \
|
||||||
make \
|
make \
|
||||||
ncurses \
|
ncurses \
|
||||||
|
|||||||
@@ -259,8 +259,10 @@ endif
|
|||||||
if ENABLE_IMAGES
|
if ENABLE_IMAGES
|
||||||
dist_tmux_SOURCES += \
|
dist_tmux_SOURCES += \
|
||||||
image.c \
|
image.c \
|
||||||
|
image-decode.c \
|
||||||
image-grid.c \
|
image-grid.c \
|
||||||
image-sixel.c \
|
image-sixel.c \
|
||||||
|
image-kitty.c \
|
||||||
image-fallback.c
|
image-fallback.c
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|||||||
38
configure.ac
38
configure.ac
@@ -557,6 +557,8 @@ fi
|
|||||||
AM_CONDITIONAL(HAVE_KEYCTL, [test "x$found_keyctl" = xyes])
|
AM_CONDITIONAL(HAVE_KEYCTL, [test "x$found_keyctl" = xyes])
|
||||||
|
|
||||||
# Enable image support.
|
# Enable image support.
|
||||||
|
libpng_version=off
|
||||||
|
libz_version=off
|
||||||
AC_ARG_ENABLE(
|
AC_ARG_ENABLE(
|
||||||
images,
|
images,
|
||||||
AS_HELP_STRING(--enable-images, enable image support)
|
AS_HELP_STRING(--enable-images, enable image support)
|
||||||
@@ -571,6 +573,40 @@ AC_ARG_ENABLE(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
if test "x$enable_images" = xyes; then
|
if test "x$enable_images" = xyes; then
|
||||||
|
PKG_CHECK_MODULES(
|
||||||
|
LIBZ,
|
||||||
|
zlib,
|
||||||
|
[
|
||||||
|
AM_CPPFLAGS="$LIBZ_CFLAGS $AM_CPPFLAGS"
|
||||||
|
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
|
||||||
|
LIBS="$LIBZ_LIBS $LIBS"
|
||||||
|
AC_DEFINE(HAVE_LIBZ)
|
||||||
|
libz_version=`$PKG_CONFIG --modversion zlib 2>/dev/null`
|
||||||
|
test "x$libz_version" = x && libz_version=on
|
||||||
|
],
|
||||||
|
[
|
||||||
|
AC_CHECK_HEADER(zlib.h, [
|
||||||
|
AC_CHECK_LIB(z, uncompress, [
|
||||||
|
LIBS="-lz $LIBS"
|
||||||
|
AC_DEFINE(HAVE_LIBZ)
|
||||||
|
libz_version=on
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
PKG_CHECK_MODULES(
|
||||||
|
LIBPNG,
|
||||||
|
[libpng >= 1.6],
|
||||||
|
[
|
||||||
|
AM_CPPFLAGS="$LIBPNG_CFLAGS $AM_CPPFLAGS"
|
||||||
|
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
|
||||||
|
LIBS="$LIBPNG_LIBS $LIBS"
|
||||||
|
AC_DEFINE(HAVE_LIBPNG)
|
||||||
|
libpng_version=`$PKG_CONFIG --modversion libpng 2>/dev/null`
|
||||||
|
test "x$libpng_version" = x && libpng_version=on
|
||||||
|
],
|
||||||
|
[:]
|
||||||
|
)
|
||||||
AC_DEFINE(ENABLE_IMAGES)
|
AC_DEFINE(ENABLE_IMAGES)
|
||||||
fi
|
fi
|
||||||
AM_CONDITIONAL(ENABLE_IMAGES, [test "x$enable_images" = xyes])
|
AM_CONDITIONAL(ENABLE_IMAGES, [test "x$enable_images" = xyes])
|
||||||
@@ -1184,6 +1220,8 @@ else
|
|||||||
fi
|
fi
|
||||||
AC_MSG_NOTICE([jemalloc: $jemalloc_version])
|
AC_MSG_NOTICE([jemalloc: $jemalloc_version])
|
||||||
AC_MSG_NOTICE([libevent: $libevent_version])
|
AC_MSG_NOTICE([libevent: $libevent_version])
|
||||||
|
AC_MSG_NOTICE([libpng: $libpng_version])
|
||||||
|
AC_MSG_NOTICE([libz: $libz_version])
|
||||||
AC_MSG_NOTICE([ncurses: $ncurses_version])
|
AC_MSG_NOTICE([ncurses: $ncurses_version])
|
||||||
AC_MSG_NOTICE([systemd: $systemd_version])
|
AC_MSG_NOTICE([systemd: $systemd_version])
|
||||||
AC_MSG_NOTICE([utempter: $utempter_version])
|
AC_MSG_NOTICE([utempter: $utempter_version])
|
||||||
|
|||||||
65
grid-view.c
65
grid-view.c
@@ -110,6 +110,11 @@ void
|
|||||||
grid_view_scroll_region_up(struct grid *gd, u_int rupper, u_int rlower,
|
grid_view_scroll_region_up(struct grid *gd, u_int rupper, u_int rlower,
|
||||||
u_int bg)
|
u_int bg)
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
struct image_saved *saved;
|
||||||
|
#endif
|
||||||
|
u_int n = rlower - rupper;
|
||||||
|
|
||||||
if (gd->flags & GRID_HISTORY) {
|
if (gd->flags & GRID_HISTORY) {
|
||||||
grid_collect_history(gd, 0);
|
grid_collect_history(gd, 0);
|
||||||
if (rupper == 0 && rlower == gd->sy - 1)
|
if (rupper == 0 && rlower == gd->sy - 1)
|
||||||
@@ -117,12 +122,24 @@ grid_view_scroll_region_up(struct grid *gd, u_int rupper, u_int rlower,
|
|||||||
else {
|
else {
|
||||||
rupper = grid_view_y(gd, rupper);
|
rupper = grid_view_y(gd, rupper);
|
||||||
rlower = grid_view_y(gd, rlower);
|
rlower = grid_view_y(gd, rlower);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
saved = image_grid_save_lines(gd, rupper, rupper + 1, n, 1);
|
||||||
|
#endif
|
||||||
grid_scroll_history_region(gd, rupper, rlower, bg);
|
grid_scroll_history_region(gd, rupper, rlower, bg);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_grid_restore_lines(gd, saved);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rupper = grid_view_y(gd, rupper);
|
rupper = grid_view_y(gd, rupper);
|
||||||
rlower = grid_view_y(gd, rlower);
|
rlower = grid_view_y(gd, rlower);
|
||||||
grid_move_lines(gd, rupper, rupper + 1, rlower - rupper, bg);
|
#ifdef ENABLE_IMAGES
|
||||||
|
saved = image_grid_save_lines(gd, rupper, rupper + 1, n, 1);
|
||||||
|
#endif
|
||||||
|
grid_move_lines(gd, rupper, rupper + 1, n, bg);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_grid_restore_lines(gd, saved);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,23 +148,42 @@ void
|
|||||||
grid_view_scroll_region_down(struct grid *gd, u_int rupper, u_int rlower,
|
grid_view_scroll_region_down(struct grid *gd, u_int rupper, u_int rlower,
|
||||||
u_int bg)
|
u_int bg)
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
struct image_saved *saved;
|
||||||
|
#endif
|
||||||
|
|
||||||
rupper = grid_view_y(gd, rupper);
|
rupper = grid_view_y(gd, rupper);
|
||||||
rlower = grid_view_y(gd, rlower);
|
rlower = grid_view_y(gd, rlower);
|
||||||
|
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
saved = image_grid_save_lines(gd, rupper + 1, rupper, rlower - rupper, 1);
|
||||||
|
#endif
|
||||||
grid_move_lines(gd, rupper + 1, rupper, rlower - rupper, bg);
|
grid_move_lines(gd, rupper + 1, rupper, rlower - rupper, bg);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_grid_restore_lines(gd, saved);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Insert lines. */
|
/* Insert lines. */
|
||||||
void
|
void
|
||||||
grid_view_insert_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
grid_view_insert_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
struct image_saved *saved;
|
||||||
|
#endif
|
||||||
u_int sy;
|
u_int sy;
|
||||||
|
|
||||||
py = grid_view_y(gd, py);
|
py = grid_view_y(gd, py);
|
||||||
|
|
||||||
sy = grid_view_y(gd, gd->sy);
|
sy = grid_view_y(gd, gd->sy);
|
||||||
|
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
saved = image_grid_save_lines(gd, py + ny, py, sy - py - ny, 0);
|
||||||
|
#endif
|
||||||
grid_move_lines(gd, py + ny, py, sy - py - ny, bg);
|
grid_move_lines(gd, py + ny, py, sy - py - ny, bg);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_grid_restore_lines(gd, saved);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Insert lines in region. */
|
/* Insert lines in region. */
|
||||||
@@ -155,6 +191,9 @@ void
|
|||||||
grid_view_insert_lines_region(struct grid *gd, u_int rlower, u_int py,
|
grid_view_insert_lines_region(struct grid *gd, u_int rlower, u_int py,
|
||||||
u_int ny, u_int bg)
|
u_int ny, u_int bg)
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
struct image_saved *saved;
|
||||||
|
#endif
|
||||||
u_int ny2;
|
u_int ny2;
|
||||||
|
|
||||||
rlower = grid_view_y(gd, rlower);
|
rlower = grid_view_y(gd, rlower);
|
||||||
@@ -162,7 +201,13 @@ grid_view_insert_lines_region(struct grid *gd, u_int rlower, u_int py,
|
|||||||
py = grid_view_y(gd, py);
|
py = grid_view_y(gd, py);
|
||||||
|
|
||||||
ny2 = rlower + 1 - py - ny;
|
ny2 = rlower + 1 - py - ny;
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
saved = image_grid_save_lines(gd, rlower + 1 - ny2, py, ny2, 0);
|
||||||
|
#endif
|
||||||
grid_move_lines(gd, rlower + 1 - ny2, py, ny2, bg);
|
grid_move_lines(gd, rlower + 1 - ny2, py, ny2, bg);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_grid_restore_lines(gd, saved);
|
||||||
|
#endif
|
||||||
grid_clear(gd, 0, py + ny2, gd->sx, ny - ny2, bg);
|
grid_clear(gd, 0, py + ny2, gd->sx, ny - ny2, bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,13 +215,22 @@ grid_view_insert_lines_region(struct grid *gd, u_int rlower, u_int py,
|
|||||||
void
|
void
|
||||||
grid_view_delete_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
grid_view_delete_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
struct image_saved *saved;
|
||||||
|
#endif
|
||||||
u_int sy;
|
u_int sy;
|
||||||
|
|
||||||
py = grid_view_y(gd, py);
|
py = grid_view_y(gd, py);
|
||||||
|
|
||||||
sy = grid_view_y(gd, gd->sy);
|
sy = grid_view_y(gd, gd->sy);
|
||||||
|
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
saved = image_grid_save_lines(gd, py, py + ny, sy - py - ny, 0);
|
||||||
|
#endif
|
||||||
grid_move_lines(gd, py, py + ny, sy - py - ny, bg);
|
grid_move_lines(gd, py, py + ny, sy - py - ny, bg);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_grid_restore_lines(gd, saved);
|
||||||
|
#endif
|
||||||
grid_clear(gd, 0, sy - ny, gd->sx, ny, bg);
|
grid_clear(gd, 0, sy - ny, gd->sx, ny, bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +239,9 @@ void
|
|||||||
grid_view_delete_lines_region(struct grid *gd, u_int rlower, u_int py,
|
grid_view_delete_lines_region(struct grid *gd, u_int rlower, u_int py,
|
||||||
u_int ny, u_int bg)
|
u_int ny, u_int bg)
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
struct image_saved *saved;
|
||||||
|
#endif
|
||||||
u_int ny2;
|
u_int ny2;
|
||||||
|
|
||||||
rlower = grid_view_y(gd, rlower);
|
rlower = grid_view_y(gd, rlower);
|
||||||
@@ -192,7 +249,13 @@ grid_view_delete_lines_region(struct grid *gd, u_int rlower, u_int py,
|
|||||||
py = grid_view_y(gd, py);
|
py = grid_view_y(gd, py);
|
||||||
|
|
||||||
ny2 = rlower + 1 - py - ny;
|
ny2 = rlower + 1 - py - ny;
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
saved = image_grid_save_lines(gd, py, py + ny, ny2, 0);
|
||||||
|
#endif
|
||||||
grid_move_lines(gd, py, py + ny, ny2, bg);
|
grid_move_lines(gd, py, py + ny, ny2, bg);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_grid_restore_lines(gd, saved);
|
||||||
|
#endif
|
||||||
grid_clear(gd, 0, py + ny2, gd->sx, ny - ny2, bg);
|
grid_clear(gd, 0, py + ny2, gd->sx, ny - ny2, bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
grid.c
13
grid.c
@@ -699,7 +699,7 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
|
|||||||
if (grid_check_y(gd, __func__, py + ny - 1) != 0)
|
if (grid_check_y(gd, __func__, py + ny - 1) != 0)
|
||||||
return;
|
return;
|
||||||
#ifdef ENABLE_IMAGES
|
#ifdef ENABLE_IMAGES
|
||||||
image_grid_remove(gd, px, py, nx, ny);
|
image_grid_overwrite(gd, px, py, nx, ny);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
for (yy = py; yy < py + ny; yy++) {
|
for (yy = py; yy < py + ny; yy++) {
|
||||||
@@ -726,6 +726,9 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
|
|||||||
void
|
void
|
||||||
grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
struct image_line *images;
|
||||||
|
#endif
|
||||||
u_int yy;
|
u_int yy;
|
||||||
|
|
||||||
if (ny == 0)
|
if (ny == 0)
|
||||||
@@ -737,8 +740,16 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
for (yy = py; yy < py + ny; yy++) {
|
for (yy = py; yy < py + ny; yy++) {
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_grid_overwrite(gd, 0, yy, gd->sx, 1);
|
||||||
|
images = gd->linedata[yy].images;
|
||||||
|
gd->linedata[yy].images = NULL;
|
||||||
|
#endif
|
||||||
grid_free_line(gd, yy);
|
grid_free_line(gd, yy);
|
||||||
grid_empty_line(gd, yy, bg);
|
grid_empty_line(gd, yy, bg);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
gd->linedata[yy].images = images;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
if (py != 0)
|
if (py != 0)
|
||||||
gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED;
|
gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED;
|
||||||
|
|||||||
208
image-decode.c
Normal file
208
image-decode.c
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/* $OpenBSD$ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||||
|
* 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>
|
||||||
|
#ifdef HAVE_LIBPNG
|
||||||
|
#include <png.h>
|
||||||
|
#endif
|
||||||
|
#include <resolv.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#ifdef HAVE_LIBZ
|
||||||
|
#include <zlib.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "tmux.h"
|
||||||
|
|
||||||
|
/* Decode a Base64 payload. */
|
||||||
|
u_char *
|
||||||
|
image_base64_decode(const char *data, size_t size, size_t limit, size_t *osize)
|
||||||
|
{
|
||||||
|
char *copy;
|
||||||
|
u_char *o;
|
||||||
|
size_t needed, padded, padding;
|
||||||
|
int n;
|
||||||
|
|
||||||
|
if (size > SIZE_MAX - 3)
|
||||||
|
return (NULL);
|
||||||
|
padding = (4 - size % 4) % 4;
|
||||||
|
if (padding == 3)
|
||||||
|
return (NULL);
|
||||||
|
padded = size + padding;
|
||||||
|
needed = padded / 4 * 3;
|
||||||
|
if (needed > limit || needed > INT_MAX)
|
||||||
|
return (NULL);
|
||||||
|
|
||||||
|
copy = xmalloc(padded + 1);
|
||||||
|
memcpy(copy, data, size);
|
||||||
|
memset(copy + size, '=', padding);
|
||||||
|
copy[padded] = '\0';
|
||||||
|
|
||||||
|
if (needed == 0)
|
||||||
|
o = xmalloc(1);
|
||||||
|
else
|
||||||
|
o = xmalloc(needed);
|
||||||
|
n = b64_pton(copy, o, needed);
|
||||||
|
free(copy);
|
||||||
|
if (n < 0) {
|
||||||
|
free(o);
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
*osize = n;
|
||||||
|
return (o);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decompress a payload. */
|
||||||
|
#ifdef HAVE_LIBZ
|
||||||
|
static u_char *
|
||||||
|
image_zlib_decode(const u_char *data, size_t size, size_t expected,
|
||||||
|
size_t limit)
|
||||||
|
{
|
||||||
|
u_char *r;
|
||||||
|
uLongf rsize;
|
||||||
|
|
||||||
|
if (expected == 0 ||
|
||||||
|
expected > limit ||
|
||||||
|
size > limit ||
|
||||||
|
expected > ULONG_MAX ||
|
||||||
|
size > ULONG_MAX)
|
||||||
|
return (NULL);
|
||||||
|
rsize = expected;
|
||||||
|
|
||||||
|
r = xmalloc(expected);
|
||||||
|
if (uncompress(r, &rsize, data, size) != Z_OK || rsize != expected) {
|
||||||
|
free(r);
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
return (r);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static u_char *
|
||||||
|
image_zlib_decode(__unused const u_char *data, __unused size_t size,
|
||||||
|
__unused size_t expected, __unused size_t limit)
|
||||||
|
{
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Decode PNG data into RGBA pixels. */
|
||||||
|
#ifdef HAVE_LIBPNG
|
||||||
|
u_char *
|
||||||
|
image_png_decode(const u_char *data, size_t size, size_t limit, int z,
|
||||||
|
size_t expected, u_int *width, u_int *height)
|
||||||
|
{
|
||||||
|
png_image pi;
|
||||||
|
u_char *r = NULL, *p = NULL;
|
||||||
|
|
||||||
|
if (size == 0 || size > limit)
|
||||||
|
return (NULL);
|
||||||
|
if (z) {
|
||||||
|
r = image_zlib_decode(data, size, expected, limit);
|
||||||
|
if (r == NULL)
|
||||||
|
return (NULL);
|
||||||
|
data = r;
|
||||||
|
size = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&pi, 0, sizeof pi);
|
||||||
|
pi.version = PNG_IMAGE_VERSION;
|
||||||
|
if (!png_image_begin_read_from_memory(&pi, data, size))
|
||||||
|
goto out;
|
||||||
|
pi.format = PNG_FORMAT_RGBA;
|
||||||
|
|
||||||
|
if (pi.width == 0 ||
|
||||||
|
pi.height == 0 ||
|
||||||
|
(uint64_t)pi.width * pi.height > limit / 4)
|
||||||
|
goto out;
|
||||||
|
p = xmalloc(PNG_IMAGE_SIZE(pi));
|
||||||
|
|
||||||
|
if (!png_image_finish_read(&pi, NULL, p, 0, NULL)) {
|
||||||
|
free(p);
|
||||||
|
p = NULL;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
*width = pi.width;
|
||||||
|
*height = pi.height;
|
||||||
|
|
||||||
|
out:
|
||||||
|
png_image_free(&pi);
|
||||||
|
free(r);
|
||||||
|
return (p);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
u_char *
|
||||||
|
image_png_decode(__unused const u_char *data, __unused size_t size,
|
||||||
|
__unused size_t limit, __unused int z, __unused size_t expected,
|
||||||
|
__unused u_int *width, __unused u_int *height)
|
||||||
|
{
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Decode raw RGB or RGBA data. */
|
||||||
|
u_char *
|
||||||
|
image_raw_decode(const u_char *data, size_t size, size_t limit, u_int format,
|
||||||
|
u_int width, u_int height, int z)
|
||||||
|
{
|
||||||
|
u_char *r = NULL, *o;
|
||||||
|
size_t expected, i, j;
|
||||||
|
u_int bytes;
|
||||||
|
|
||||||
|
if (format != 24 && format != 32)
|
||||||
|
return (NULL);
|
||||||
|
|
||||||
|
if (width == 0 ||
|
||||||
|
height == 0 ||
|
||||||
|
size > limit ||
|
||||||
|
(uint64_t)width * height > limit / 4)
|
||||||
|
return (NULL);
|
||||||
|
bytes = (format == 24 ? 3 : 4);
|
||||||
|
expected = (size_t)width * height * bytes;
|
||||||
|
|
||||||
|
if (z) {
|
||||||
|
r = image_zlib_decode(data, size, expected, limit);
|
||||||
|
if (r == NULL)
|
||||||
|
return (NULL);
|
||||||
|
data = r;
|
||||||
|
} else {
|
||||||
|
if (size != expected)
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes == 4) {
|
||||||
|
if (r != NULL)
|
||||||
|
return (r);
|
||||||
|
o = xmalloc(expected);
|
||||||
|
memcpy(o, data, expected);
|
||||||
|
return (o);
|
||||||
|
}
|
||||||
|
|
||||||
|
o = xmalloc((size_t)width * height * 4);
|
||||||
|
for (i = j = 0; i < expected; i += 3, j += 4) {
|
||||||
|
o[j + 0] = data[i + 0];
|
||||||
|
o[j + 1] = data[i + 1];
|
||||||
|
o[j + 2] = data[i + 2];
|
||||||
|
o[j + 3] = 255;
|
||||||
|
}
|
||||||
|
free(r);
|
||||||
|
return (o);
|
||||||
|
}
|
||||||
422
image-grid.c
422
image-grid.c
@@ -23,6 +23,9 @@
|
|||||||
|
|
||||||
#include "tmux.h"
|
#include "tmux.h"
|
||||||
|
|
||||||
|
/* Next image placement index. */
|
||||||
|
static uint64_t next_image_placement_index;
|
||||||
|
|
||||||
/* A run of cells on one grid line. */
|
/* A run of cells on one grid line. */
|
||||||
struct image_span {
|
struct image_span {
|
||||||
u_int x;
|
u_int x;
|
||||||
@@ -41,17 +44,19 @@ TAILQ_HEAD(image_spans, image_span);
|
|||||||
/* Spans on one grid line. */
|
/* Spans on one grid line. */
|
||||||
struct image_line {
|
struct image_line {
|
||||||
struct image_spans spans;
|
struct image_spans spans;
|
||||||
|
int in_region; /* Inside the current scroll region. */
|
||||||
};
|
};
|
||||||
|
|
||||||
/* A placement of an image, may be across multiple lines */
|
/* A placement of an image, may be across multiple lines */
|
||||||
struct image_placement {
|
struct image_placement {
|
||||||
struct image_store *store;
|
struct image_store *store;
|
||||||
struct image *image;
|
struct image *image;
|
||||||
|
uint64_t index;
|
||||||
struct image_spans spans;
|
struct image_spans spans;
|
||||||
|
|
||||||
/* Original column and first source column not yet placed. */
|
|
||||||
u_int x;
|
u_int x;
|
||||||
u_int recover_from;
|
u_int recover_from;
|
||||||
|
int scroll; /* -1 unchecked, 0 fixed, 1 moving. */
|
||||||
|
|
||||||
TAILQ_ENTRY(image_placement) entry;
|
TAILQ_ENTRY(image_placement) entry;
|
||||||
};
|
};
|
||||||
@@ -67,6 +72,16 @@ TAILQ_HEAD(image_stores, image_store);
|
|||||||
/* All current placements. */
|
/* All current placements. */
|
||||||
static struct image_stores image_stores = TAILQ_HEAD_INITIALIZER(image_stores);
|
static struct image_stores image_stores = TAILQ_HEAD_INITIALIZER(image_stores);
|
||||||
|
|
||||||
|
/* Images saved while grid lines are moved. */
|
||||||
|
struct image_saved {
|
||||||
|
u_int dy;
|
||||||
|
u_int py;
|
||||||
|
u_int ny;
|
||||||
|
u_int hsize;
|
||||||
|
int scroll;
|
||||||
|
struct image_line **lines;
|
||||||
|
};
|
||||||
|
|
||||||
/* A shared placements copied when duplicating lines. */
|
/* A shared placements copied when duplicating lines. */
|
||||||
struct image_copy_map {
|
struct image_copy_map {
|
||||||
struct image_placement *source;
|
struct image_placement *source;
|
||||||
@@ -88,7 +103,7 @@ struct image_recover_row {
|
|||||||
u_int source_y;
|
u_int source_y;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* A backend to draw an image. */
|
/* Image backend callbacks. */
|
||||||
struct image_backend {
|
struct image_backend {
|
||||||
const char *name;
|
const char *name;
|
||||||
int (*get_flags)(struct tty *);
|
int (*get_flags)(struct tty *);
|
||||||
@@ -100,6 +115,7 @@ struct image_backend {
|
|||||||
void (*start_draw)(struct tty *, u_int, u_int, u_int, u_int);
|
void (*start_draw)(struct tty *, u_int, u_int, u_int, u_int);
|
||||||
void (*flush)(struct tty *);
|
void (*flush)(struct tty *);
|
||||||
void (*free)(struct tty *);
|
void (*free)(struct tty *);
|
||||||
|
void (*remove_image)(struct image *);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Backend definitions. */
|
/* Backend definitions. */
|
||||||
@@ -111,6 +127,7 @@ static struct image_backend image_backend_fallback = {
|
|||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
|
NULL,
|
||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
static struct image_backend image_backend_sixel = {
|
static struct image_backend image_backend_sixel = {
|
||||||
@@ -121,11 +138,24 @@ static struct image_backend image_backend_sixel = {
|
|||||||
sixel_draw_rect,
|
sixel_draw_rect,
|
||||||
sixel_start_draw,
|
sixel_start_draw,
|
||||||
sixel_flush_pending,
|
sixel_flush_pending,
|
||||||
sixel_free_pending
|
sixel_free_pending,
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
static struct image_backend image_backend_kitty = {
|
||||||
|
"kitty",
|
||||||
|
NULL,
|
||||||
|
kitty_select,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
kitty_remove_image
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Backends in order of preference. */
|
/* Backends in order of preference. */
|
||||||
static struct image_backend *image_backends[] = {
|
static struct image_backend *image_backends[] = {
|
||||||
|
&image_backend_kitty,
|
||||||
&image_backend_sixel,
|
&image_backend_sixel,
|
||||||
&image_backend_fallback
|
&image_backend_fallback
|
||||||
};
|
};
|
||||||
@@ -190,7 +220,7 @@ image_draw_span(struct image_backend *backend, struct tty *tty,
|
|||||||
backend->draw_rect(tty, &rect);
|
backend->draw_rect(tty, &rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Draw images in the visible part of one screen line. */
|
/* Draw images after text on one screen line. */
|
||||||
void
|
void
|
||||||
image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py,
|
image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py,
|
||||||
u_int nx, u_int atx, u_int aty)
|
u_int nx, u_int atx, u_int aty)
|
||||||
@@ -198,7 +228,7 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py,
|
|||||||
struct image_backend *backend;
|
struct image_backend *backend;
|
||||||
struct image_line *line;
|
struct image_line *line;
|
||||||
struct image_span *span;
|
struct image_span *span;
|
||||||
u_int start, end, span_end;
|
u_int start, end, span_end, gy;
|
||||||
|
|
||||||
image_tty_update(tty);
|
image_tty_update(tty);
|
||||||
backend = tty->image_backend;
|
backend = tty->image_backend;
|
||||||
@@ -206,7 +236,8 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py,
|
|||||||
return;
|
return;
|
||||||
if (py >= s->grid->sy)
|
if (py >= s->grid->sy)
|
||||||
return;
|
return;
|
||||||
line = s->grid->linedata[s->grid->hsize + py].images;
|
gy = s->grid->hsize + py;
|
||||||
|
line = s->grid->linedata[gy].images;
|
||||||
if (line == NULL)
|
if (line == NULL)
|
||||||
return;
|
return;
|
||||||
end = px + nx;
|
end = px + nx;
|
||||||
@@ -297,6 +328,7 @@ image_placement_create(struct grid *gd, struct image *im)
|
|||||||
plmt = xcalloc(1, sizeof *plmt);
|
plmt = xcalloc(1, sizeof *plmt);
|
||||||
plmt->store = store;
|
plmt->store = store;
|
||||||
plmt->image = im;
|
plmt->image = im;
|
||||||
|
plmt->index = ++next_image_placement_index;
|
||||||
TAILQ_INIT(&plmt->spans);
|
TAILQ_INIT(&plmt->spans);
|
||||||
TAILQ_INSERT_TAIL(&store->placements, plmt, entry);
|
TAILQ_INSERT_TAIL(&store->placements, plmt, entry);
|
||||||
image_ref(im);
|
image_ref(im);
|
||||||
@@ -313,7 +345,42 @@ image_placement_free(struct image_placement *plmt)
|
|||||||
free(plmt);
|
free(plmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Attach a span to both its grid line and its placement. */
|
/* Draw images in source order, then placement order. */
|
||||||
|
static int
|
||||||
|
image_compare_placements(struct image_placement *a, struct image_placement *b)
|
||||||
|
{
|
||||||
|
u_int aorder, border;
|
||||||
|
|
||||||
|
aorder = image_get_order_id(a->image);
|
||||||
|
border = image_get_order_id(b->image);
|
||||||
|
if (aorder != border)
|
||||||
|
return (aorder < border ? -1 : 1);
|
||||||
|
if (a->index != b->index)
|
||||||
|
return (a->index < b->index ? -1 : 1);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Insert a span into a grid line. */
|
||||||
|
static void
|
||||||
|
image_span_insert(struct image_line *line, struct image_span *span)
|
||||||
|
{
|
||||||
|
struct image_placement *plmt = span->placement;
|
||||||
|
struct image_span *at;
|
||||||
|
int c;
|
||||||
|
|
||||||
|
span->line = line;
|
||||||
|
TAILQ_FOREACH(at, &line->spans, line_entry) {
|
||||||
|
c = image_compare_placements(plmt, at->placement);
|
||||||
|
if (c < 0 || (plmt == at->placement && span->x < at->x)) {
|
||||||
|
TAILQ_INSERT_BEFORE(at, span, line_entry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (at == NULL)
|
||||||
|
TAILQ_INSERT_TAIL(&line->spans, span, line_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attach a span in drawing order to its grid line and placement. */
|
||||||
static void
|
static void
|
||||||
image_span_add(struct image_line *line, struct image_placement *plmt, u_int x,
|
image_span_add(struct image_line *line, struct image_placement *plmt, u_int x,
|
||||||
u_int sx, u_int src_x, u_int src_y)
|
u_int sx, u_int src_x, u_int src_y)
|
||||||
@@ -322,15 +389,13 @@ image_span_add(struct image_line *line, struct image_placement *plmt, u_int x,
|
|||||||
|
|
||||||
if (sx == 0)
|
if (sx == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
span = xcalloc(1, sizeof *span);
|
span = xcalloc(1, sizeof *span);
|
||||||
span->x = x;
|
span->x = x;
|
||||||
span->sx = sx;
|
span->sx = sx;
|
||||||
span->source_x = src_x;
|
span->source_x = src_x;
|
||||||
span->source_y = src_y;
|
span->source_y = src_y;
|
||||||
span->line = line;
|
|
||||||
span->placement = plmt;
|
span->placement = plmt;
|
||||||
TAILQ_INSERT_TAIL(&line->spans, span, line_entry);
|
image_span_insert(line, span);
|
||||||
TAILQ_INSERT_TAIL(&plmt->spans, span, placement_entry);
|
TAILQ_INSERT_TAIL(&plmt->spans, span, placement_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,11 +481,12 @@ image_span_remove(struct image_span *span, u_int x, u_int end)
|
|||||||
image_span_free(span);
|
image_span_free(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove a range of cells from every span on a line. */
|
/* Remove overwritten spans. */
|
||||||
static void
|
static void
|
||||||
image_line_remove(struct image_line *line, u_int x, u_int w)
|
image_line_overwrite(struct image_line *line, u_int x, u_int w)
|
||||||
{
|
{
|
||||||
struct image_span *span, *next;
|
struct image_span *span, *next;
|
||||||
|
enum image_type type;
|
||||||
u_int end;
|
u_int end;
|
||||||
|
|
||||||
if (line == NULL || w == 0)
|
if (line == NULL || w == 0)
|
||||||
@@ -428,13 +494,16 @@ image_line_remove(struct image_line *line, u_int x, u_int w)
|
|||||||
end = x + w;
|
end = x + w;
|
||||||
if (end < x)
|
if (end < x)
|
||||||
end = UINT_MAX;
|
end = UINT_MAX;
|
||||||
TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next)
|
TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) {
|
||||||
|
type = image_get_type(span->placement->image);
|
||||||
|
if (type != IMAGE_TYPE_KITTY)
|
||||||
image_span_remove(span, x, end);
|
image_span_remove(span, x, end);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove a rectangle of image spans from a grid. */
|
/* Remove SIXEL spans overwritten by text writes or erases. */
|
||||||
void
|
void
|
||||||
image_grid_remove(struct grid *gd, u_int x, u_int y, u_int sx, u_int sy)
|
image_grid_overwrite(struct grid *gd, u_int x, u_int y, u_int sx, u_int sy)
|
||||||
{
|
{
|
||||||
u_int rows = gd->hsize + gd->sy;
|
u_int rows = gd->hsize + gd->sy;
|
||||||
u_int row;
|
u_int row;
|
||||||
@@ -444,10 +513,30 @@ image_grid_remove(struct grid *gd, u_int x, u_int y, u_int sx, u_int sy)
|
|||||||
if (sy > rows - y)
|
if (sy > rows - y)
|
||||||
sy = rows - y;
|
sy = rows - y;
|
||||||
for (row = y; row < y + sy; row++)
|
for (row = y; row < y + sy; row++)
|
||||||
image_line_remove(gd->linedata[row].images, x, sx);
|
image_line_overwrite(gd->linedata[row].images, x, sx);
|
||||||
image_store_prune(gd->images);
|
image_store_prune(gd->images);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Check whether a placement has any spans in the visible cells. */
|
||||||
|
static int
|
||||||
|
image_placement_visible(struct grid *gd, struct image_placement *plmt)
|
||||||
|
{
|
||||||
|
struct image_line *line;
|
||||||
|
struct image_span *span;
|
||||||
|
u_int row;
|
||||||
|
|
||||||
|
for (row = gd->hsize; row < gd->hsize + gd->sy; row++) {
|
||||||
|
line = gd->linedata[row].images;
|
||||||
|
if (line == NULL)
|
||||||
|
continue;
|
||||||
|
TAILQ_FOREACH(span, &line->spans, line_entry) {
|
||||||
|
if (span->placement == plmt && span->x < gd->sx)
|
||||||
|
return (1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
/* Remove a placement and all of its spans. */
|
/* Remove a placement and all of its spans. */
|
||||||
static void
|
static void
|
||||||
image_remove_placement(struct image_placement *plmt)
|
image_remove_placement(struct image_placement *plmt)
|
||||||
@@ -459,12 +548,77 @@ image_remove_placement(struct image_placement *plmt)
|
|||||||
image_placement_free(plmt);
|
image_placement_free(plmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Check whether a grid retains any placements of an image. */
|
||||||
|
int
|
||||||
|
image_grid_has_image(struct grid *gd, u_int id)
|
||||||
|
{
|
||||||
|
struct image_placement *plmt;
|
||||||
|
|
||||||
|
if (gd->images == NULL)
|
||||||
|
return (0);
|
||||||
|
TAILQ_FOREACH(plmt, &gd->images->placements, entry) {
|
||||||
|
if (image_get_id(plmt->image) == id)
|
||||||
|
return (1);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove all placements of an image from a grid. */
|
||||||
|
u_int
|
||||||
|
image_grid_clear(struct grid *gd, u_int id)
|
||||||
|
{
|
||||||
|
struct image_placement *plmt, *plmt1;
|
||||||
|
u_int removed = 0;
|
||||||
|
|
||||||
|
if (gd->images == NULL)
|
||||||
|
return (0);
|
||||||
|
TAILQ_FOREACH_SAFE(plmt, &gd->images->placements, entry, plmt1) {
|
||||||
|
if (image_get_id(plmt->image) != id)
|
||||||
|
continue;
|
||||||
|
image_remove_placement(plmt);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
return (removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove whole placements of a type which have visible spans. */
|
||||||
|
u_int
|
||||||
|
image_grid_clear_visible(struct grid *gd, enum image_type type)
|
||||||
|
{
|
||||||
|
struct image_placement *plmt, *plmt1;
|
||||||
|
u_int removed = 0;
|
||||||
|
|
||||||
|
if (gd->images == NULL)
|
||||||
|
return (0);
|
||||||
|
TAILQ_FOREACH_SAFE(plmt, &gd->images->placements, entry, plmt1) {
|
||||||
|
if (image_get_type(plmt->image) != type)
|
||||||
|
continue;
|
||||||
|
if (!image_placement_visible(gd, plmt))
|
||||||
|
continue;
|
||||||
|
image_remove_placement(plmt);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
return (removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove placements of an image from a screen. */
|
||||||
|
void
|
||||||
|
image_clear(struct screen_write_ctx *ctx, u_int id)
|
||||||
|
{
|
||||||
|
image_grid_clear(ctx->s->grid, id);
|
||||||
|
if (ctx->s->saved_grid != NULL)
|
||||||
|
image_grid_clear(ctx->s->saved_grid, id);
|
||||||
|
if (ctx->wp != NULL)
|
||||||
|
ctx->wp->flags |= PANE_REDRAW;
|
||||||
|
}
|
||||||
|
|
||||||
/* Remove every placement for an image. */
|
/* Remove every placement for an image. */
|
||||||
void
|
void
|
||||||
image_grid_remove_all(struct image *im)
|
image_grid_remove_all(struct image *im)
|
||||||
{
|
{
|
||||||
struct image_store *store;
|
struct image_store *store;
|
||||||
struct image_placement *plmt, *next;
|
struct image_placement *plmt, *next;
|
||||||
|
u_int i;
|
||||||
|
|
||||||
TAILQ_FOREACH(store, &image_stores, entry) {
|
TAILQ_FOREACH(store, &image_stores, entry) {
|
||||||
TAILQ_FOREACH_SAFE(plmt, &store->placements, entry, next) {
|
TAILQ_FOREACH_SAFE(plmt, &store->placements, entry, next) {
|
||||||
@@ -472,6 +626,10 @@ image_grid_remove_all(struct image *im)
|
|||||||
image_remove_placement(plmt);
|
image_remove_placement(plmt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (i = 0; i < nitems(image_backends); i++) {
|
||||||
|
if (image_backends[i]->remove_image != NULL)
|
||||||
|
image_backends[i]->remove_image(im);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Free all spans on a grid line. */
|
/* Free all spans on a grid line. */
|
||||||
@@ -532,6 +690,8 @@ image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx)
|
|||||||
if (span_end > end)
|
if (span_end > end)
|
||||||
span_end = end;
|
span_end = end;
|
||||||
|
|
||||||
|
if (image_get_type(span->placement->image) == IMAGE_TYPE_KITTY)
|
||||||
|
continue;
|
||||||
moves = xreallocarray(moves, n + 1, sizeof *moves);
|
moves = xreallocarray(moves, n + 1, sizeof *moves);
|
||||||
moves[n].placement = span->placement;
|
moves[n].placement = span->placement;
|
||||||
moves[n].x = dx + start - px;
|
moves[n].x = dx + start - px;
|
||||||
@@ -541,8 +701,8 @@ image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx)
|
|||||||
span->placement->recover_from = 0;
|
span->placement->recover_from = 0;
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
image_line_remove(line, px, nx);
|
image_line_overwrite(line, px, nx);
|
||||||
image_line_remove(line, dx, nx);
|
image_line_overwrite(line, dx, nx);
|
||||||
for (i = 0; i < n; i++) {
|
for (i = 0; i < n; i++) {
|
||||||
image_span_add(line, moves[i].placement, moves[i].x,
|
image_span_add(line, moves[i].placement, moves[i].x,
|
||||||
moves[i].sx, moves[i].source_x, moves[i].source_y);
|
moves[i].sx, moves[i].source_x, moves[i].source_y);
|
||||||
@@ -551,7 +711,175 @@ image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx)
|
|||||||
image_store_prune(gd->images);
|
image_store_prune(gd->images);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Map one source placement to one destination placement during a copy. */
|
/* Mark placements to scroll, except those crossing a scroll region edge. */
|
||||||
|
static void
|
||||||
|
image_grid_mark_scroll(struct grid *gd, u_int upper, u_int lower)
|
||||||
|
{
|
||||||
|
struct image_line *line;
|
||||||
|
struct image_placement *plmt;
|
||||||
|
struct image_span *span, *at;
|
||||||
|
u_int y, hsize = gd->hsize;
|
||||||
|
|
||||||
|
for (y = upper; y <= lower; y++) {
|
||||||
|
line = gd->linedata[y].images;
|
||||||
|
if (line == NULL)
|
||||||
|
continue;
|
||||||
|
line->in_region = 1;
|
||||||
|
TAILQ_FOREACH(span, &line->spans, line_entry)
|
||||||
|
span->placement->scroll = -1;
|
||||||
|
}
|
||||||
|
for (y = upper; y <= lower; y++) {
|
||||||
|
line = gd->linedata[y].images;
|
||||||
|
if (line == NULL)
|
||||||
|
continue;
|
||||||
|
TAILQ_FOREACH(span, &line->spans, line_entry) {
|
||||||
|
plmt = span->placement;
|
||||||
|
if (plmt->scroll != -1)
|
||||||
|
continue;
|
||||||
|
plmt->scroll = 1;
|
||||||
|
if (upper == hsize && lower == hsize + gd->sy - 1)
|
||||||
|
continue;
|
||||||
|
TAILQ_FOREACH(at, &plmt->spans, placement_entry) {
|
||||||
|
if (!at->line->in_region) {
|
||||||
|
plmt->scroll = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Move spans between lines. */
|
||||||
|
static void
|
||||||
|
image_line_move(struct grid_line *dst, struct grid_line *src, int scroll)
|
||||||
|
{
|
||||||
|
struct image_line *line = src->images, *to = NULL;
|
||||||
|
struct image_span *span, *next;
|
||||||
|
enum image_type type;
|
||||||
|
|
||||||
|
if (line == NULL)
|
||||||
|
return;
|
||||||
|
TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) {
|
||||||
|
type = image_get_type(span->placement->image);
|
||||||
|
if (type == IMAGE_TYPE_KITTY &&
|
||||||
|
(!scroll || !span->placement->scroll))
|
||||||
|
continue;
|
||||||
|
if (dst == NULL)
|
||||||
|
image_span_free(span);
|
||||||
|
else {
|
||||||
|
if (to == NULL)
|
||||||
|
to = image_line_get(dst);
|
||||||
|
TAILQ_REMOVE(&line->spans, span, line_entry);
|
||||||
|
image_span_insert(to, span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Move spans if needed. */
|
||||||
|
static void
|
||||||
|
image_grid_move_lines(struct grid *gd, u_int dy, u_int py, u_int ny, int scroll)
|
||||||
|
{
|
||||||
|
struct image_line *line;
|
||||||
|
u_int rows = gd->hsize + gd->sy;
|
||||||
|
u_int upper, lower, y, dst, src;
|
||||||
|
|
||||||
|
if (gd->images == NULL ||
|
||||||
|
ny == 0 ||
|
||||||
|
dy == py ||
|
||||||
|
dy >= rows ||
|
||||||
|
py >= rows ||
|
||||||
|
ny > rows - dy ||
|
||||||
|
ny > rows - py)
|
||||||
|
return;
|
||||||
|
upper = (dy < py ? dy : py);
|
||||||
|
lower = (dy > py ? dy : py) + ny - 1;
|
||||||
|
if (scroll)
|
||||||
|
image_grid_mark_scroll(gd, upper, lower);
|
||||||
|
|
||||||
|
for (y = dy; y < dy + ny; y++) {
|
||||||
|
if (y < py || y >= py + ny)
|
||||||
|
image_line_move(NULL, &gd->linedata[y], scroll);
|
||||||
|
}
|
||||||
|
for (y = 0; y < ny; y++) {
|
||||||
|
if (dy < py) {
|
||||||
|
dst = dy + y;
|
||||||
|
src = py + y;
|
||||||
|
} else {
|
||||||
|
dst = dy + ny - 1 - y;
|
||||||
|
src = py + ny - 1 - y;
|
||||||
|
}
|
||||||
|
image_line_move(&gd->linedata[dst], &gd->linedata[src], scroll);
|
||||||
|
}
|
||||||
|
if (scroll) {
|
||||||
|
for (y = upper; y <= lower; y++) {
|
||||||
|
line = gd->linedata[y].images;
|
||||||
|
if (line != NULL)
|
||||||
|
line->in_region = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image_store_prune(gd->images);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detach images before moving lines. */
|
||||||
|
struct image_saved *
|
||||||
|
image_grid_save_lines(struct grid *gd, u_int dy, u_int py, u_int ny, int scroll)
|
||||||
|
{
|
||||||
|
struct image_saved *saved;
|
||||||
|
u_int rows = gd->hsize + gd->sy;
|
||||||
|
u_int upper, lower, y, n;
|
||||||
|
|
||||||
|
if (gd->images == NULL || ny == 0 || dy == py ||
|
||||||
|
dy >= rows || py >= rows || ny > rows - dy || ny > rows - py)
|
||||||
|
return (NULL);
|
||||||
|
upper = (dy < py ? dy : py);
|
||||||
|
lower = (dy > py ? dy : py) + ny - 1;
|
||||||
|
for (y = upper; y <= lower; y++) {
|
||||||
|
if (gd->linedata[y].images != NULL)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (y > lower)
|
||||||
|
return (NULL);
|
||||||
|
|
||||||
|
n = lower - upper + 1;
|
||||||
|
saved = xcalloc(1, sizeof *saved);
|
||||||
|
saved->lines = xcalloc(n, sizeof *saved->lines);
|
||||||
|
saved->dy = dy;
|
||||||
|
saved->py = py;
|
||||||
|
saved->ny = ny;
|
||||||
|
saved->hsize = gd->hsize;
|
||||||
|
saved->scroll = scroll;
|
||||||
|
for (y = upper; y <= lower; y++) {
|
||||||
|
saved->lines[y - upper] = gd->linedata[y].images;
|
||||||
|
gd->linedata[y].images = NULL;
|
||||||
|
}
|
||||||
|
return (saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Restore images and move if needed. */
|
||||||
|
void
|
||||||
|
image_grid_restore_lines(struct grid *gd, struct image_saved *saved)
|
||||||
|
{
|
||||||
|
u_int dy, py, upper, lower, y, offset;
|
||||||
|
|
||||||
|
if (saved == NULL)
|
||||||
|
return;
|
||||||
|
|
||||||
|
offset = gd->hsize - saved->hsize;
|
||||||
|
dy = saved->dy + offset;
|
||||||
|
py = saved->py + offset;
|
||||||
|
upper = (dy < py ? dy : py);
|
||||||
|
lower = (dy > py ? dy : py) + saved->ny - 1;
|
||||||
|
for (y = upper; y <= lower; y++)
|
||||||
|
gd->linedata[y].images = saved->lines[y - upper];
|
||||||
|
|
||||||
|
if (offset != 0)
|
||||||
|
image_grid_move_lines(gd, saved->hsize, dy, 1, 0);
|
||||||
|
image_grid_move_lines(gd, dy, py, saved->ny, saved->scroll);
|
||||||
|
free(saved->lines);
|
||||||
|
free(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map placements during a copy. */
|
||||||
static struct image_placement *
|
static struct image_placement *
|
||||||
image_copy_placement(struct grid *dst, struct image_placement *src,
|
image_copy_placement(struct grid *dst, struct image_placement *src,
|
||||||
struct image_copy_map **maps, u_int *nmaps)
|
struct image_copy_map **maps, u_int *nmaps)
|
||||||
@@ -565,6 +893,7 @@ image_copy_placement(struct grid *dst, struct image_placement *src,
|
|||||||
return (map[i].destination);
|
return (map[i].destination);
|
||||||
}
|
}
|
||||||
plmt = image_placement_create(dst, src->image);
|
plmt = image_placement_create(dst, src->image);
|
||||||
|
plmt->index = src->index;
|
||||||
plmt->x = src->x;
|
plmt->x = src->x;
|
||||||
plmt->recover_from = src->recover_from;
|
plmt->recover_from = src->recover_from;
|
||||||
|
|
||||||
@@ -745,7 +1074,7 @@ image_damage_screen(struct screen_write_ctx *ctx, u_int x, u_int y, u_int sx,
|
|||||||
|
|
||||||
image_redraw_area(ctx, x, y, sx, sy);
|
image_redraw_area(ctx, x, y, sx, sy);
|
||||||
if (ctx->flags & SCREEN_WRITE_INPUT)
|
if (ctx->flags & SCREEN_WRITE_INPUT)
|
||||||
image_grid_remove(s->grid, x, s->grid->hsize + y, sx, sy);
|
image_grid_overwrite(s->grid, x, s->grid->hsize + y, sx, sy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check whether an image cell has any visible pixels. */
|
/* Check whether an image cell has any visible pixels. */
|
||||||
@@ -803,7 +1132,7 @@ image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old,
|
|||||||
return (1);
|
return (1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Discard cells of older placements fully covered by a new image. */
|
/* Discard older SIXEL cells fully covered by a new SIXEL image. */
|
||||||
static void
|
static void
|
||||||
image_line_cover(struct image_line *line, struct image_placement *plmt,
|
image_line_cover(struct image_line *line, struct image_placement *plmt,
|
||||||
u_int x, u_int src_x, u_int src_y)
|
u_int x, u_int src_x, u_int src_y)
|
||||||
@@ -813,7 +1142,11 @@ image_line_cover(struct image_line *line, struct image_placement *plmt,
|
|||||||
|
|
||||||
TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) {
|
TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) {
|
||||||
old = span->placement;
|
old = span->placement;
|
||||||
if (old == plmt || x < span->x || x >= span->x + span->sx)
|
if (old == plmt)
|
||||||
|
continue;
|
||||||
|
if (image_get_type(old->image) != IMAGE_TYPE_SIXEL)
|
||||||
|
continue;
|
||||||
|
if (x < span->x || x >= span->x + span->sx)
|
||||||
continue;
|
continue;
|
||||||
if (!image_cell_covers(plmt->image, src_x, src_y,
|
if (!image_cell_covers(plmt->image, src_x, src_y,
|
||||||
old->image, span->source_x + x - span->x,
|
old->image, span->source_x + x - span->x,
|
||||||
@@ -831,6 +1164,10 @@ image_add_row(struct image_line *line, struct image_placement *plmt,
|
|||||||
struct image *im = plmt->image;
|
struct image *im = plmt->image;
|
||||||
u_int x, i, run;
|
u_int x, i, run;
|
||||||
|
|
||||||
|
if (image_get_type(im) == IMAGE_TYPE_KITTY) {
|
||||||
|
image_span_add(line, plmt, cx, sx, 0, src_y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (x = 0; x < sx; x += run) {
|
for (x = 0; x < sx; x += run) {
|
||||||
if (!image_cell_has_visible_pixels(im, x, src_y)) {
|
if (!image_cell_has_visible_pixels(im, x, src_y)) {
|
||||||
run = 1;
|
run = 1;
|
||||||
@@ -840,6 +1177,7 @@ image_add_row(struct image_line *line, struct image_placement *plmt,
|
|||||||
if (!image_cell_has_visible_pixels(im, x + run, src_y))
|
if (!image_cell_has_visible_pixels(im, x + run, src_y))
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < run; i++)
|
for (i = 0; i < run; i++)
|
||||||
image_line_cover(line, plmt, cx + x + i, x + i, src_y);
|
image_line_cover(line, plmt, cx + x + i, x + i, src_y);
|
||||||
image_span_add(line, plmt, cx + x, run, x, src_y);
|
image_span_add(line, plmt, cx + x, run, x, src_y);
|
||||||
@@ -854,6 +1192,10 @@ image_extend_row(struct image_line *line, struct image_placement *plmt,
|
|||||||
struct image *im = plmt->image;
|
struct image *im = plmt->image;
|
||||||
u_int x, run;
|
u_int x, run;
|
||||||
|
|
||||||
|
if (image_get_type(im) == IMAGE_TYPE_KITTY) {
|
||||||
|
image_span_add(line, plmt, cx + from, to - from, from, src_y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (x = from; x < to; x += run) {
|
for (x = from; x < to; x += run) {
|
||||||
if (!image_cell_has_visible_pixels(im, x, src_y)) {
|
if (!image_cell_has_visible_pixels(im, x, src_y)) {
|
||||||
run = 1;
|
run = 1;
|
||||||
@@ -915,9 +1257,10 @@ image_grid_resize_width(struct grid *gd, u_int new_sx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Place a SIXEL image at the cursor. */
|
/* Place an image at the cursor. */
|
||||||
void
|
void
|
||||||
image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg)
|
image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg,
|
||||||
|
int no_cursor)
|
||||||
{
|
{
|
||||||
struct image_geom *ig = image_get_geometry(im);
|
struct image_geom *ig = image_get_geometry(im);
|
||||||
struct screen *s = ctx->s;
|
struct screen *s = ctx->s;
|
||||||
@@ -927,6 +1270,7 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg)
|
|||||||
struct image_line *line;
|
struct image_line *line;
|
||||||
u_int cx = s->cx, cy = s->cy, y, lines;
|
u_int cx = s->cx, cy = s->cy, y, lines;
|
||||||
u_int sx = ig->sx, sy = ig->sy;
|
u_int sx = ig->sx, sy = ig->sy;
|
||||||
|
u_int cursor_x = 0, cursor_rows;
|
||||||
u_int origin_y = 0, h_origin_y, region_height;
|
u_int origin_y = 0, h_origin_y, region_height;
|
||||||
u_int remaining, n;
|
u_int remaining, n;
|
||||||
|
|
||||||
@@ -935,9 +1279,17 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg)
|
|||||||
if (sx == 0)
|
if (sx == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
/* Kitty moves to the last image row; SIXEL advances past it. */
|
||||||
|
cursor_rows = sy;
|
||||||
|
if (image_get_type(im) == IMAGE_TYPE_KITTY)
|
||||||
|
cursor_rows--;
|
||||||
|
|
||||||
/* Scroll images that move the cursor. */
|
/* Scroll images that move the cursor. */
|
||||||
if (screen_size_y(s) - cy <= sy) {
|
if (no_cursor) {
|
||||||
lines = sy - (screen_size_y(s) - cy) + 1;
|
if (sy > screen_size_y(s) - cy)
|
||||||
|
sy = screen_size_y(s) - cy;
|
||||||
|
} else if (screen_size_y(s) - cy <= cursor_rows) {
|
||||||
|
lines = cursor_rows - (screen_size_y(s) - cy) + 1;
|
||||||
region_height = s->rlower - s->rupper + 1;
|
region_height = s->rlower - s->rupper + 1;
|
||||||
|
|
||||||
remaining = lines;
|
remaining = lines;
|
||||||
@@ -959,6 +1311,7 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg)
|
|||||||
}
|
}
|
||||||
cy = s->cy;
|
cy = s->cy;
|
||||||
sy -= origin_y;
|
sy -= origin_y;
|
||||||
|
cursor_rows -= origin_y;
|
||||||
}
|
}
|
||||||
plmt = image_placement_create(gd, im);
|
plmt = image_placement_create(gd, im);
|
||||||
if (sx < ig->sx) {
|
if (sx < ig->sx) {
|
||||||
@@ -982,10 +1335,14 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg)
|
|||||||
image_add_row(line, plmt, cx, origin_y + y, sx);
|
image_add_row(line, plmt, cx, origin_y + y, sx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Schedule a redraw and advance the cursor past the image. */
|
/* Schedule a redraw and move the cursor. */
|
||||||
image_store_prune(gd->images);
|
image_store_prune(gd->images);
|
||||||
image_redraw_area(ctx, cx, cy, sx, sy);
|
image_redraw_area(ctx, cx, cy, sx, sy);
|
||||||
screen_write_cursormove(ctx, 0, cy + sy, 0);
|
if (!no_cursor) {
|
||||||
|
if (image_get_type(im) == IMAGE_TYPE_KITTY)
|
||||||
|
cursor_x = cx + sx;
|
||||||
|
screen_write_cursormove(ctx, cursor_x, cy + cursor_rows, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Find the cell within an image at a grid position. */
|
/* Find the cell within an image at a grid position. */
|
||||||
@@ -995,6 +1352,8 @@ image_grid_get_at(struct grid *gd, u_int x, u_int y, struct image **im,
|
|||||||
{
|
{
|
||||||
struct image_line *line;
|
struct image_line *line;
|
||||||
struct image_span *span, *found = NULL;
|
struct image_span *span, *found = NULL;
|
||||||
|
struct image *pim;
|
||||||
|
u_int px;
|
||||||
|
|
||||||
if (y >= gd->hsize + gd->sy)
|
if (y >= gd->hsize + gd->sy)
|
||||||
return (0);
|
return (0);
|
||||||
@@ -1002,7 +1361,12 @@ image_grid_get_at(struct grid *gd, u_int x, u_int y, struct image **im,
|
|||||||
if (line == NULL)
|
if (line == NULL)
|
||||||
return (0);
|
return (0);
|
||||||
TAILQ_FOREACH(span, &line->spans, line_entry) {
|
TAILQ_FOREACH(span, &line->spans, line_entry) {
|
||||||
if (x >= span->x && x < span->x + span->sx)
|
if (x < span->x || x >= span->x + span->sx)
|
||||||
|
continue;
|
||||||
|
pim = span->placement->image;
|
||||||
|
px = span->source_x + x - span->x;
|
||||||
|
if (!image_cell_has_visible_pixels(pim, px, span->source_y))
|
||||||
|
continue;
|
||||||
found = span;
|
found = span;
|
||||||
}
|
}
|
||||||
if (found == NULL)
|
if (found == NULL)
|
||||||
|
|||||||
781
image-kitty.c
Normal file
781
image-kitty.c
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
/* $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, FITNESS AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
|
||||||
|
* THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "tmux.h"
|
||||||
|
|
||||||
|
/* State attached to an input parser. */
|
||||||
|
struct kitty_state {
|
||||||
|
char action;
|
||||||
|
u_int format;
|
||||||
|
char medium;
|
||||||
|
char compression;
|
||||||
|
char delete;
|
||||||
|
u_int width;
|
||||||
|
u_int height;
|
||||||
|
u_int source_x;
|
||||||
|
u_int source_y;
|
||||||
|
u_int source_width;
|
||||||
|
u_int source_height;
|
||||||
|
u_int x_offset;
|
||||||
|
u_int y_offset;
|
||||||
|
u_int columns;
|
||||||
|
u_int rows;
|
||||||
|
u_int image_id;
|
||||||
|
u_int image_number;
|
||||||
|
u_int placement_id;
|
||||||
|
u_int quiet;
|
||||||
|
u_int data_size;
|
||||||
|
int no_cursor;
|
||||||
|
int more;
|
||||||
|
|
||||||
|
u_char *data;
|
||||||
|
size_t size;
|
||||||
|
size_t allocated;
|
||||||
|
|
||||||
|
TAILQ_HEAD(, kitty_image_map) images;
|
||||||
|
TAILQ_ENTRY(kitty_state) entry;
|
||||||
|
};
|
||||||
|
TAILQ_HEAD(kitty_states, kitty_state);
|
||||||
|
|
||||||
|
/* Kitty input states holding uploaded images. */
|
||||||
|
static struct kitty_states kitty_states = TAILQ_HEAD_INITIALIZER(kitty_states);
|
||||||
|
|
||||||
|
/* Map a Kitty placement ID to an image ID. */
|
||||||
|
struct kitty_placement_map {
|
||||||
|
u_int kitty_id;
|
||||||
|
u_int image_id;
|
||||||
|
|
||||||
|
TAILQ_ENTRY(kitty_placement_map) entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Map Kitty image ID to image ID and placement views. */
|
||||||
|
struct kitty_image_map {
|
||||||
|
u_int kitty_id;
|
||||||
|
u_int image_id;
|
||||||
|
TAILQ_HEAD(, kitty_placement_map) placements;
|
||||||
|
|
||||||
|
TAILQ_ENTRY(kitty_image_map) entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Select Kitty output. */
|
||||||
|
int
|
||||||
|
kitty_select(__unused struct tty *tty)
|
||||||
|
{
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse Kitty graphics control data. */
|
||||||
|
static int
|
||||||
|
kitty_parse_control_data(struct kitty_state *ks, const u_char *data,
|
||||||
|
size_t size, const char **m)
|
||||||
|
{
|
||||||
|
char *copy, *ptr, *next, *value;
|
||||||
|
const char *errstr;
|
||||||
|
u_int number;
|
||||||
|
char key;
|
||||||
|
int z;
|
||||||
|
|
||||||
|
*m = NULL;
|
||||||
|
|
||||||
|
copy = ptr = xstrndup(data, size);
|
||||||
|
while ((next = strsep(&ptr, ",")) != NULL) {
|
||||||
|
if (*next == '\0') {
|
||||||
|
if (ptr == NULL)
|
||||||
|
break;
|
||||||
|
*m = (*m == NULL ? "EINVAL" : *m);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
key = next[0];
|
||||||
|
if (next[1] != '=' || next[2] == '\0') {
|
||||||
|
*m = (*m == NULL ? "EINVAL" : *m);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value = next + 2;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'a':
|
||||||
|
case 't':
|
||||||
|
case 'o':
|
||||||
|
case 'd':
|
||||||
|
if (value[1] != '\0') {
|
||||||
|
*m = (*m == NULL ? "EINVAL" : *m);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case 'a':
|
||||||
|
ks->action = value[0];
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
ks->medium = value[0];
|
||||||
|
break;
|
||||||
|
case 'o':
|
||||||
|
ks->compression = value[0];
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
ks->delete = value[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'z':
|
||||||
|
z = strtonum(value, INT_MIN, INT_MAX, &errstr);
|
||||||
|
if (errstr != NULL)
|
||||||
|
*m = (*m == NULL ? "EINVAL" : *m);
|
||||||
|
else if (z != 0)
|
||||||
|
*m = (*m == NULL ? "ENOTSUP" : *m);
|
||||||
|
break;
|
||||||
|
case 'U':
|
||||||
|
case 'P':
|
||||||
|
case 'Q':
|
||||||
|
case 'H':
|
||||||
|
case 'V':
|
||||||
|
if (strtonum(value, INT_MIN, UINT_MAX, &errstr) != 0)
|
||||||
|
*m = (*m == NULL ? "ENOTSUP" : *m);
|
||||||
|
else if (errstr != NULL)
|
||||||
|
*m = (*m == NULL ? "ENOTSUP" : *m);
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
case 's':
|
||||||
|
case 'v':
|
||||||
|
case 'i':
|
||||||
|
case 'I':
|
||||||
|
case 'p':
|
||||||
|
case 'q':
|
||||||
|
case 'm':
|
||||||
|
case 'S':
|
||||||
|
case 'x':
|
||||||
|
case 'y':
|
||||||
|
case 'w':
|
||||||
|
case 'h':
|
||||||
|
case 'X':
|
||||||
|
case 'Y':
|
||||||
|
case 'c':
|
||||||
|
case 'r':
|
||||||
|
case 'C':
|
||||||
|
number = strtonum(value, 0, UINT_MAX, &errstr);
|
||||||
|
if (errstr != NULL) {
|
||||||
|
*m = (*m == NULL ? "EINVAL" : *m);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case 'f':
|
||||||
|
ks->format = number;
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
ks->width = number;
|
||||||
|
break;
|
||||||
|
case 'v':
|
||||||
|
ks->height = number;
|
||||||
|
break;
|
||||||
|
case 'i':
|
||||||
|
ks->image_id = number;
|
||||||
|
break;
|
||||||
|
case 'I':
|
||||||
|
ks->image_number = number;
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
ks->placement_id = number;
|
||||||
|
break;
|
||||||
|
case 'q':
|
||||||
|
ks->quiet = number;
|
||||||
|
break;
|
||||||
|
case 'm':
|
||||||
|
ks->more = (number != 0);
|
||||||
|
break;
|
||||||
|
case 'S':
|
||||||
|
ks->data_size = number;
|
||||||
|
break;
|
||||||
|
case 'x':
|
||||||
|
ks->source_x = number;
|
||||||
|
break;
|
||||||
|
case 'y':
|
||||||
|
ks->source_y = number;
|
||||||
|
break;
|
||||||
|
case 'w':
|
||||||
|
ks->source_width = number;
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
ks->source_height = number;
|
||||||
|
break;
|
||||||
|
case 'X':
|
||||||
|
ks->x_offset = number;
|
||||||
|
break;
|
||||||
|
case 'Y':
|
||||||
|
ks->y_offset = number;
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
ks->columns = number;
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
ks->rows = number;
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
ks->no_cursor = (number != 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(copy);
|
||||||
|
if (*m != NULL)
|
||||||
|
return (-1);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Free Kitty graphics parser state. */
|
||||||
|
void
|
||||||
|
kitty_free_state(void *state)
|
||||||
|
{
|
||||||
|
struct kitty_state *ks = state;
|
||||||
|
struct kitty_image_map *map;
|
||||||
|
struct kitty_placement_map *kpl;
|
||||||
|
|
||||||
|
if (ks == NULL)
|
||||||
|
return;
|
||||||
|
TAILQ_REMOVE(&kitty_states, ks, entry);
|
||||||
|
while ((map = TAILQ_FIRST(&ks->images)) != NULL) {
|
||||||
|
TAILQ_REMOVE(&ks->images, map, entry);
|
||||||
|
while ((kpl = TAILQ_FIRST(&map->placements)) != NULL) {
|
||||||
|
TAILQ_REMOVE(&map->placements, kpl, entry);
|
||||||
|
free(kpl);
|
||||||
|
}
|
||||||
|
image_free(image_find(map->image_id));
|
||||||
|
free(map);
|
||||||
|
}
|
||||||
|
free(ks->data);
|
||||||
|
free(ks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove a Kitty image. */
|
||||||
|
void
|
||||||
|
kitty_remove_image(struct image *im)
|
||||||
|
{
|
||||||
|
struct kitty_state *ks;
|
||||||
|
struct kitty_image_map *map, *next;
|
||||||
|
struct kitty_placement_map *kpl, *kpl1;
|
||||||
|
u_int id = image_get_id(im);
|
||||||
|
|
||||||
|
TAILQ_FOREACH(ks, &kitty_states, entry) {
|
||||||
|
TAILQ_FOREACH_SAFE(map, &ks->images, entry, next) {
|
||||||
|
TAILQ_FOREACH_SAFE(kpl, &map->placements, entry, kpl1) {
|
||||||
|
if (map->image_id != id && kpl->image_id != id)
|
||||||
|
continue;
|
||||||
|
TAILQ_REMOVE(&map->placements, kpl, entry);
|
||||||
|
free(kpl);
|
||||||
|
}
|
||||||
|
if (map->image_id != id)
|
||||||
|
continue;
|
||||||
|
TAILQ_REMOVE(&ks->images, map, entry);
|
||||||
|
image_free(im);
|
||||||
|
free(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find an image mapping by Kitty image ID. */
|
||||||
|
static struct kitty_image_map *
|
||||||
|
kitty_image_map_find(struct kitty_state *ks, u_int id)
|
||||||
|
{
|
||||||
|
struct kitty_image_map *map;
|
||||||
|
|
||||||
|
TAILQ_FOREACH(map, &ks->images, entry) {
|
||||||
|
if (map->kitty_id == id)
|
||||||
|
return (map);
|
||||||
|
}
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Replace the image associated with a Kitty image ID. */
|
||||||
|
static struct kitty_image_map *
|
||||||
|
kitty_image_map_set(struct kitty_state *ks, struct screen_write_ctx *ctx,
|
||||||
|
u_int id, struct image *im)
|
||||||
|
{
|
||||||
|
struct kitty_image_map *map;
|
||||||
|
struct kitty_placement_map *kpl;
|
||||||
|
struct image *old;
|
||||||
|
|
||||||
|
if (id == 0)
|
||||||
|
return (NULL);
|
||||||
|
map = kitty_image_map_find(ks, id);
|
||||||
|
if (map != NULL) {
|
||||||
|
old = image_find(map->image_id);
|
||||||
|
image_set_order_id(im, image_get_order_id(old));
|
||||||
|
while ((kpl = TAILQ_FIRST(&map->placements)) != NULL) {
|
||||||
|
TAILQ_REMOVE(&map->placements, kpl, entry);
|
||||||
|
image_clear(ctx, kpl->image_id);
|
||||||
|
free(kpl);
|
||||||
|
}
|
||||||
|
image_free(image_find(map->image_id));
|
||||||
|
} else {
|
||||||
|
map = xcalloc(1, sizeof *map);
|
||||||
|
map->kitty_id = id;
|
||||||
|
TAILQ_INIT(&map->placements);
|
||||||
|
TAILQ_INSERT_HEAD(&ks->images, map, entry);
|
||||||
|
}
|
||||||
|
image_ref(im);
|
||||||
|
map->image_id = image_get_id(im);
|
||||||
|
return (map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Replace a named placement, or add a new anonymous placement. */
|
||||||
|
static void
|
||||||
|
kitty_placement_map_set(struct screen_write_ctx *ctx,
|
||||||
|
struct kitty_image_map *map, u_int id, struct image *im)
|
||||||
|
{
|
||||||
|
struct kitty_placement_map *kpl, *kpl1;
|
||||||
|
|
||||||
|
if (map == NULL)
|
||||||
|
return;
|
||||||
|
TAILQ_FOREACH_SAFE(kpl, &map->placements, entry, kpl1) {
|
||||||
|
if (image_find(kpl->image_id) == NULL) {
|
||||||
|
TAILQ_REMOVE(&map->placements, kpl, entry);
|
||||||
|
free(kpl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (id != 0 && kpl->kitty_id == id)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (kpl == NULL) {
|
||||||
|
kpl = xcalloc(1, sizeof *kpl);
|
||||||
|
kpl->kitty_id = id;
|
||||||
|
TAILQ_INSERT_TAIL(&map->placements, kpl, entry);
|
||||||
|
} else
|
||||||
|
image_clear(ctx, kpl->image_id);
|
||||||
|
kpl->image_id = image_get_id(im);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove mappings for absent placements. */
|
||||||
|
static u_int
|
||||||
|
kitty_prune_placements(struct kitty_image_map *map, struct screen *s)
|
||||||
|
{
|
||||||
|
struct kitty_placement_map *kpl, *kpl1;
|
||||||
|
u_int removed = 0;
|
||||||
|
struct grid *sgd = s->saved_grid;
|
||||||
|
|
||||||
|
TAILQ_FOREACH_SAFE(kpl, &map->placements, entry, kpl1) {
|
||||||
|
if (image_grid_has_image(s->grid, kpl->image_id))
|
||||||
|
continue;
|
||||||
|
if (sgd != NULL && image_grid_has_image(sgd, kpl->image_id))
|
||||||
|
continue;
|
||||||
|
TAILQ_REMOVE(&map->placements, kpl, entry);
|
||||||
|
free(kpl);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
return (removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete selected Kitty placements. */
|
||||||
|
static void
|
||||||
|
kitty_delete_images(struct kitty_state *ks, struct screen_write_ctx *ctx)
|
||||||
|
{
|
||||||
|
struct screen *s = ctx->s;
|
||||||
|
struct grid *gd = s->grid;
|
||||||
|
struct kitty_image_map *map, *map1;
|
||||||
|
struct kitty_placement_map *kpl;
|
||||||
|
u_int removed;
|
||||||
|
char how = ks->delete;
|
||||||
|
int release;
|
||||||
|
|
||||||
|
release = (how >= 'A' && how <= 'Z');
|
||||||
|
if (release)
|
||||||
|
how += 'a' - 'A';
|
||||||
|
TAILQ_FOREACH(map, &ks->images, entry)
|
||||||
|
kitty_prune_placements(map, s);
|
||||||
|
if (how == 'a')
|
||||||
|
image_grid_clear_visible(gd, IMAGE_TYPE_KITTY);
|
||||||
|
|
||||||
|
TAILQ_FOREACH_SAFE(map, &ks->images, entry, map1) {
|
||||||
|
removed = 0;
|
||||||
|
if (how == 'i') {
|
||||||
|
if (map->kitty_id != ks->image_id)
|
||||||
|
continue;
|
||||||
|
TAILQ_FOREACH(kpl, &map->placements, entry) {
|
||||||
|
if (ks->placement_id != 0 &&
|
||||||
|
kpl->kitty_id != ks->placement_id)
|
||||||
|
continue;
|
||||||
|
removed += image_grid_clear(gd, kpl->image_id);
|
||||||
|
}
|
||||||
|
if (ks->placement_id == 0)
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
removed += kitty_prune_placements(map, s);
|
||||||
|
if (!release || removed == 0 || !TAILQ_EMPTY(&map->placements))
|
||||||
|
continue;
|
||||||
|
TAILQ_REMOVE(&ks->images, map, entry);
|
||||||
|
image_free(image_find(map->image_id));
|
||||||
|
free(map);
|
||||||
|
}
|
||||||
|
if (ctx->wp != NULL)
|
||||||
|
ctx->wp->flags |= PANE_REDRAW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decode and append one base64-encoded Kitty payload chunk. */
|
||||||
|
static int
|
||||||
|
kitty_append_data(struct kitty_state *ks, const u_char *data, size_t size)
|
||||||
|
{
|
||||||
|
u_char *d;
|
||||||
|
size_t dsize, allocated;
|
||||||
|
|
||||||
|
if (size > IMAGE_SIZE_LIMIT)
|
||||||
|
return (-1);
|
||||||
|
|
||||||
|
d = image_base64_decode(data, size, IMAGE_SIZE_LIMIT, &dsize);
|
||||||
|
if (d == NULL)
|
||||||
|
return (-1);
|
||||||
|
if (dsize == 0) {
|
||||||
|
free(d);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (dsize > IMAGE_SIZE_LIMIT - ks->size) {
|
||||||
|
free(d);
|
||||||
|
return (-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ks->size + dsize > ks->allocated) {
|
||||||
|
allocated = ks->allocated;
|
||||||
|
if (allocated == 0)
|
||||||
|
allocated = 4096;
|
||||||
|
while (allocated < ks->size + dsize)
|
||||||
|
allocated *= 2;
|
||||||
|
ks->data = xrealloc(ks->data, allocated);
|
||||||
|
ks->allocated = allocated;
|
||||||
|
}
|
||||||
|
memcpy(ks->data + ks->size, d, dsize);
|
||||||
|
ks->size += dsize;
|
||||||
|
|
||||||
|
free(d);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reply to a Kitty graphics sequence. */
|
||||||
|
static void
|
||||||
|
kitty_send_reply(struct input_ctx *ictx, struct kitty_state *ks, const char *m)
|
||||||
|
{
|
||||||
|
char i[96] = "";
|
||||||
|
char I[96] = "";
|
||||||
|
char p[96] = "";
|
||||||
|
|
||||||
|
if (ks->quiet >= 2)
|
||||||
|
return;
|
||||||
|
if (ks->action == 'd' && strcmp(m, "OK") == 0)
|
||||||
|
return;
|
||||||
|
if (ks->quiet == 1 && strcmp(m, "OK") == 0)
|
||||||
|
return;
|
||||||
|
if (ks->image_id == 0 && ks->image_number == 0 && ks->action != 'q')
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ks->image_id != 0 || ks->image_number == 0)
|
||||||
|
xsnprintf(i, sizeof i, "i=%u", ks->image_id);
|
||||||
|
if (ks->image_number != 0) {
|
||||||
|
if (*i != '\0')
|
||||||
|
strlcat(i, ",", sizeof i);
|
||||||
|
xsnprintf(I, sizeof I, "I=%u", ks->image_number);
|
||||||
|
}
|
||||||
|
if (ks->placement_id != 0)
|
||||||
|
xsnprintf(p, sizeof p, ",p=%u", ks->placement_id);
|
||||||
|
input_reply(ictx, 0, "\033_G%s%s%s;%s\033\\", i, I, p, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a Kitty placement with shared source pixels and stored geometry. */
|
||||||
|
static struct image *
|
||||||
|
kitty_place_image(struct image *src, struct kitty_state *ks, u_int xpixel,
|
||||||
|
u_int ypixel)
|
||||||
|
{
|
||||||
|
struct image_geom *sg = image_get_geometry(src);
|
||||||
|
enum image_type type = image_get_type(src);
|
||||||
|
struct image_geom ig = { 0 };
|
||||||
|
u_char *pixels;
|
||||||
|
size_t stride;
|
||||||
|
uint64_t cw, ch;
|
||||||
|
double size;
|
||||||
|
|
||||||
|
ig.src_x = ks->source_x;
|
||||||
|
ig.src_y = ks->source_y;
|
||||||
|
|
||||||
|
/* Clip the source rectangle. */
|
||||||
|
if (ig.src_x >= sg->src_w || ig.src_y >= sg->src_h)
|
||||||
|
return (NULL);
|
||||||
|
ig.src_w = sg->src_w - ig.src_x;
|
||||||
|
if (ks->source_width != 0 && ig.src_w > ks->source_width)
|
||||||
|
ig.src_w = ks->source_width;
|
||||||
|
ig.src_h = sg->src_h - ig.src_y;
|
||||||
|
if (ks->source_height != 0 && ig.src_h > ks->source_height)
|
||||||
|
ig.src_h = ks->source_height;
|
||||||
|
|
||||||
|
/* Clamp offsets to the starting cell. */
|
||||||
|
if (xpixel == 0)
|
||||||
|
xpixel = 8;
|
||||||
|
if (ypixel == 0)
|
||||||
|
ypixel = 16;
|
||||||
|
ig.canvas_xoff = ks->x_offset;
|
||||||
|
if (ig.canvas_xoff >= xpixel)
|
||||||
|
ig.canvas_xoff = xpixel - 1;
|
||||||
|
ig.canvas_yoff = ks->y_offset;
|
||||||
|
if (ig.canvas_yoff >= ypixel)
|
||||||
|
ig.canvas_yoff = ypixel - 1;
|
||||||
|
|
||||||
|
/* Work out the size. */
|
||||||
|
ig.sx = ks->columns;
|
||||||
|
ig.sy = ks->rows;
|
||||||
|
ig.scaled_width = ig.src_w;
|
||||||
|
ig.scaled_height = ig.src_h;
|
||||||
|
if (ig.sx == 0 && ig.sy == 0) {
|
||||||
|
if (ig.canvas_xoff > UINT_MAX - ig.src_w ||
|
||||||
|
ig.canvas_yoff > UINT_MAX - ig.src_h)
|
||||||
|
return (NULL);
|
||||||
|
image_size_in_cells(ig.src_w + ig.canvas_xoff,
|
||||||
|
ig.src_h + ig.canvas_yoff, xpixel, ypixel, &ig.sx, &ig.sy);
|
||||||
|
} else if (ig.sy == 0) {
|
||||||
|
ig.scaled_width = (double)ig.sx * xpixel - ig.canvas_xoff;
|
||||||
|
ig.scaled_height = ig.src_h * ig.scaled_width / ig.src_w;
|
||||||
|
|
||||||
|
size = ceil((ig.scaled_height + ig.canvas_yoff) / ypixel);
|
||||||
|
if (size > USHRT_MAX)
|
||||||
|
return (NULL);
|
||||||
|
ig.sy = size;
|
||||||
|
} else if (ig.sx == 0) {
|
||||||
|
ig.scaled_height = (double)ig.sy * ypixel - ig.canvas_yoff;
|
||||||
|
ig.scaled_width = ig.src_w * ig.scaled_height / ig.src_h;
|
||||||
|
|
||||||
|
size = ceil((ig.scaled_width + ig.canvas_xoff) / xpixel);
|
||||||
|
if (size > USHRT_MAX)
|
||||||
|
return (NULL);
|
||||||
|
ig.sx = size;
|
||||||
|
} else {
|
||||||
|
ig.scaled_width = (double)ig.sx * xpixel - ig.canvas_xoff;
|
||||||
|
ig.scaled_height = (double)ig.sy * ypixel - ig.canvas_yoff;
|
||||||
|
}
|
||||||
|
if (ig.sx > USHRT_MAX ||
|
||||||
|
ig.sy > USHRT_MAX ||
|
||||||
|
ig.sx * ig.sy > IMAGE_SIZE_LIMIT)
|
||||||
|
return (NULL);
|
||||||
|
|
||||||
|
/* Record geometry in the input terminal's pixel coordinates. */
|
||||||
|
cw = (uint64_t)ig.sx * xpixel;
|
||||||
|
ch = (uint64_t)ig.sy * ypixel;
|
||||||
|
if (cw > UINT_MAX || ch > UINT_MAX)
|
||||||
|
return (NULL);
|
||||||
|
ig.canvas_width = cw;
|
||||||
|
ig.canvas_height = ch;
|
||||||
|
pixels = image_get_pixels(src, &stride, NULL);
|
||||||
|
return (image_create_with_parent(type, src, &ig, stride, pixels));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse and reply to a Kitty graphics sequence. */
|
||||||
|
void
|
||||||
|
kitty_parse_input(struct input_ctx *ictx, struct window_pane *wp,
|
||||||
|
struct screen_write_ctx *ctx, u_int bg, void **state, const u_char *buf,
|
||||||
|
size_t len)
|
||||||
|
{
|
||||||
|
struct window *w = wp->window;
|
||||||
|
struct kitty_state *ks = *state;
|
||||||
|
struct kitty_state cks = { .action = 't' };
|
||||||
|
struct image_geom ig = { 0 };
|
||||||
|
struct kitty_image_map *map = NULL;
|
||||||
|
const u_char *found;
|
||||||
|
const char *m;
|
||||||
|
u_char *p = NULL;
|
||||||
|
size_t csize, psize;
|
||||||
|
u_int sx, sy;
|
||||||
|
uint64_t cw, ch;
|
||||||
|
struct image *src = NULL, *im = NULL;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
buf++;
|
||||||
|
len--;
|
||||||
|
|
||||||
|
if (ks == NULL) {
|
||||||
|
ks = xcalloc(1, sizeof *ks);
|
||||||
|
TAILQ_INIT(&ks->images);
|
||||||
|
TAILQ_INSERT_TAIL(&kitty_states, ks, entry);
|
||||||
|
*state = ks;
|
||||||
|
}
|
||||||
|
|
||||||
|
found = memchr(buf, ';', len);
|
||||||
|
if (found == NULL) {
|
||||||
|
csize = len;
|
||||||
|
psize = 0;
|
||||||
|
} else {
|
||||||
|
csize = found - buf;
|
||||||
|
psize = len - csize - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ks->more) {
|
||||||
|
kitty_parse_control_data(&cks, buf, csize, &m);
|
||||||
|
if (cks.action == 'd') {
|
||||||
|
free(ks->data);
|
||||||
|
ks->data = NULL;
|
||||||
|
ks->size = 0;
|
||||||
|
ks->allocated = 0;
|
||||||
|
ks->more = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ks->more) {
|
||||||
|
ks->action = 't';
|
||||||
|
ks->format = 32;
|
||||||
|
ks->medium = 'd';
|
||||||
|
ks->compression = 'o';
|
||||||
|
ks->delete = 'a';
|
||||||
|
ks->width = 0;
|
||||||
|
ks->height = 0;
|
||||||
|
ks->source_x = 0;
|
||||||
|
ks->source_y = 0;
|
||||||
|
ks->source_width = 0;
|
||||||
|
ks->source_height = 0;
|
||||||
|
ks->x_offset = 0;
|
||||||
|
ks->y_offset = 0;
|
||||||
|
ks->columns = 0;
|
||||||
|
ks->rows = 0;
|
||||||
|
ks->image_id = 0;
|
||||||
|
ks->image_number = 0;
|
||||||
|
ks->placement_id = 0;
|
||||||
|
ks->quiet = 0;
|
||||||
|
ks->data_size = 0;
|
||||||
|
ks->no_cursor = 0;
|
||||||
|
}
|
||||||
|
ks->more = 0;
|
||||||
|
|
||||||
|
error = kitty_parse_control_data(ks, buf, csize, &m);
|
||||||
|
if (error != 0)
|
||||||
|
goto out;
|
||||||
|
m = "EINVAL";
|
||||||
|
if (ks->image_id == 0 && ks->image_number == 0)
|
||||||
|
ks->placement_id = 0;
|
||||||
|
if (ks->image_number != 0) {
|
||||||
|
if (ks->image_id != 0)
|
||||||
|
goto out;
|
||||||
|
m = "ENOTSUP";
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (ks->medium != 'd') {
|
||||||
|
m = "ENOTSUP";
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (ks->action != 't' && ks->action != 'T' &&
|
||||||
|
ks->action != 'q' && ks->action != 'p' && ks->action != 'd') {
|
||||||
|
m = "ENOTSUP";
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (ks->action == 'd') {
|
||||||
|
switch (ks->delete) {
|
||||||
|
case 'a':
|
||||||
|
case 'A':
|
||||||
|
case 'i':
|
||||||
|
case 'I':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
m = "ENOTSUP";
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
kitty_delete_images(ks, ctx);
|
||||||
|
m = "OK";
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (psize != 0 && kitty_append_data(ks, found + 1, psize) != 0)
|
||||||
|
goto out;
|
||||||
|
if (ks->more)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ks->action == 'p') {
|
||||||
|
map = kitty_image_map_find(ks, ks->image_id);
|
||||||
|
if (map == NULL) {
|
||||||
|
m = "ENOENT";
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
src = image_find(map->image_id);
|
||||||
|
image_ref(src);
|
||||||
|
|
||||||
|
im = kitty_place_image(src, ks, w->xpixel, w->ypixel);
|
||||||
|
if (im == NULL)
|
||||||
|
goto out;
|
||||||
|
goto place;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ks->compression != 'o' && ks->compression != 'z')
|
||||||
|
goto out;
|
||||||
|
switch (ks->format) {
|
||||||
|
case 24:
|
||||||
|
case 32:
|
||||||
|
p = image_raw_decode(ks->data, ks->size, IMAGE_SIZE_LIMIT,
|
||||||
|
ks->format, ks->width, ks->height, ks->compression == 'z');
|
||||||
|
break;
|
||||||
|
case 100:
|
||||||
|
p = image_png_decode(ks->data, ks->size, IMAGE_SIZE_LIMIT,
|
||||||
|
ks->compression == 'z', ks->data_size, &ks->width,
|
||||||
|
&ks->height);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (p == NULL)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
cw = (w->xpixel == 0 ? 8 : w->xpixel);
|
||||||
|
ch = (w->ypixel == 0 ? 16 : w->ypixel);
|
||||||
|
image_size_in_cells(ks->width, ks->height, cw, ch, &sx, &sy);
|
||||||
|
cw *= sx;
|
||||||
|
ch *= sy;
|
||||||
|
if (cw > UINT_MAX || ch > UINT_MAX) {
|
||||||
|
free(p);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
ig.src_w = ig.scaled_width = ks->width;
|
||||||
|
ig.src_h = ig.scaled_height = ks->height;
|
||||||
|
ig.canvas_width = cw;
|
||||||
|
ig.canvas_height = ch;
|
||||||
|
ig.sx = sx;
|
||||||
|
ig.sy = sy;
|
||||||
|
src = image_create(IMAGE_TYPE_KITTY, &ig, p);
|
||||||
|
if (src == NULL) {
|
||||||
|
free(p);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ks->action == 'T') {
|
||||||
|
im = kitty_place_image(src, ks, w->xpixel, w->ypixel);
|
||||||
|
if (im == NULL)
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (ks->action != 'q')
|
||||||
|
map = kitty_image_map_set(ks, ctx, ks->image_id, src);
|
||||||
|
|
||||||
|
place:
|
||||||
|
if (im != NULL) {
|
||||||
|
image_set_order_id(im, image_get_order_id(src));
|
||||||
|
kitty_placement_map_set(ctx, map, ks->placement_id, im);
|
||||||
|
image_write(ctx, im, bg, ks->no_cursor);
|
||||||
|
}
|
||||||
|
m = "OK";
|
||||||
|
|
||||||
|
out:
|
||||||
|
kitty_send_reply(ictx, ks, m);
|
||||||
|
if (im != NULL)
|
||||||
|
image_free(im);
|
||||||
|
if (src != NULL)
|
||||||
|
image_free(src);
|
||||||
|
|
||||||
|
free(ks->data);
|
||||||
|
ks->data = NULL;
|
||||||
|
ks->size = 0;
|
||||||
|
ks->allocated = 0;
|
||||||
|
ks->more = 0;
|
||||||
|
}
|
||||||
@@ -667,7 +667,7 @@ sixel_to_image(struct sixel_image *si)
|
|||||||
ig.canvas_height = sy * si->ypixel;
|
ig.canvas_height = sy * si->ypixel;
|
||||||
ig.sx = sx;
|
ig.sx = sx;
|
||||||
ig.sy = sy;
|
ig.sy = sy;
|
||||||
im = image_create(&ig, pixels);
|
im = image_create(IMAGE_TYPE_SIXEL, &ig, pixels);
|
||||||
if (im == NULL)
|
if (im == NULL)
|
||||||
free(pixels);
|
free(pixels);
|
||||||
return (im);
|
return (im);
|
||||||
@@ -683,7 +683,7 @@ sixel_write(struct screen_write_ctx *ctx, struct sixel_image *si, u_int bg)
|
|||||||
sixel_free(si);
|
sixel_free(si);
|
||||||
if (im == NULL)
|
if (im == NULL)
|
||||||
return;
|
return;
|
||||||
image_write(ctx, im, bg);
|
image_write(ctx, im, bg, 0);
|
||||||
image_free(im);
|
image_free(im);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
image.c
36
image.c
@@ -34,6 +34,7 @@
|
|||||||
TAILQ_HEAD(image_list, image);
|
TAILQ_HEAD(image_list, image);
|
||||||
struct image {
|
struct image {
|
||||||
u_int id;
|
u_int id;
|
||||||
|
enum image_type type;
|
||||||
u_int references;
|
u_int references;
|
||||||
struct image_geom geom;
|
struct image_geom geom;
|
||||||
|
|
||||||
@@ -109,6 +110,27 @@ image_get_id(struct image *im)
|
|||||||
return (im->id);
|
return (im->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Get order ID of an image. */
|
||||||
|
u_int
|
||||||
|
image_get_order_id(struct image *im)
|
||||||
|
{
|
||||||
|
return (im->order_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set the order ID on an image. */
|
||||||
|
void
|
||||||
|
image_set_order_id(struct image *im, u_int id)
|
||||||
|
{
|
||||||
|
im->order_id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the type of an image. */
|
||||||
|
enum image_type
|
||||||
|
image_get_type(struct image *im)
|
||||||
|
{
|
||||||
|
return (im->type);
|
||||||
|
}
|
||||||
|
|
||||||
/* Remove an image and children. */
|
/* Remove an image and children. */
|
||||||
static void
|
static void
|
||||||
image_remove_all(struct image *im)
|
image_remove_all(struct image *im)
|
||||||
@@ -181,7 +203,8 @@ image_check_limits(struct image *keep, size_t size)
|
|||||||
|
|
||||||
/* Allocate an image and assign a unique ID. */
|
/* Allocate an image and assign a unique ID. */
|
||||||
static struct image *
|
static struct image *
|
||||||
image_alloc(struct image_geom *ig, size_t stride, u_char *pixels)
|
image_alloc(enum image_type type, struct image_geom *ig, size_t stride,
|
||||||
|
u_char *pixels)
|
||||||
{
|
{
|
||||||
struct image *im;
|
struct image *im;
|
||||||
u_int id;
|
u_int id;
|
||||||
@@ -194,6 +217,7 @@ image_alloc(struct image_geom *ig, size_t stride, u_char *pixels)
|
|||||||
|
|
||||||
im = xcalloc(1, sizeof *im);
|
im = xcalloc(1, sizeof *im);
|
||||||
im->id = id;
|
im->id = id;
|
||||||
|
im->type = type;
|
||||||
im->references = 1;
|
im->references = 1;
|
||||||
im->order_id = id;
|
im->order_id = id;
|
||||||
im->geom = *ig;
|
im->geom = *ig;
|
||||||
@@ -208,17 +232,17 @@ image_alloc(struct image_geom *ig, size_t stride, u_char *pixels)
|
|||||||
|
|
||||||
/* Create an image with the provided pixels. */
|
/* Create an image with the provided pixels. */
|
||||||
struct image *
|
struct image *
|
||||||
image_create(struct image_geom *ig, u_char *pixels)
|
image_create(enum image_type type, struct image_geom *ig, u_char *pixels)
|
||||||
{
|
{
|
||||||
size_t stride = ig->src_w * 4;
|
size_t stride = ig->src_w * 4;
|
||||||
|
|
||||||
return (image_create_with_parent(NULL, ig, stride, pixels));
|
return (image_create_with_parent(type, NULL, ig, stride, pixels));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create an image based on a parent image. */
|
/* Create an image based on a parent image. */
|
||||||
struct image *
|
struct image *
|
||||||
image_create_with_parent(struct image *parent, struct image_geom *ig,
|
image_create_with_parent(enum image_type type, struct image *parent,
|
||||||
size_t stride, u_char *pixels)
|
struct image_geom *ig, size_t stride, u_char *pixels)
|
||||||
{
|
{
|
||||||
struct image *im;
|
struct image *im;
|
||||||
size_t size = 0;
|
size_t size = 0;
|
||||||
@@ -243,7 +267,7 @@ image_create_with_parent(struct image *parent, struct image_geom *ig,
|
|||||||
if (image_check_limits(parent, size) != 0)
|
if (image_check_limits(parent, size) != 0)
|
||||||
return (NULL);
|
return (NULL);
|
||||||
|
|
||||||
im = image_alloc(ig, stride, pixels);
|
im = image_alloc(type, ig, stride, pixels);
|
||||||
im->parent = parent;
|
im->parent = parent;
|
||||||
if (parent == NULL)
|
if (parent == NULL)
|
||||||
image_size += im->size;
|
image_size += im->size;
|
||||||
|
|||||||
16
input.c
16
input.c
@@ -144,6 +144,9 @@ struct input_ctx {
|
|||||||
*/
|
*/
|
||||||
struct evbuffer *since_ground;
|
struct evbuffer *since_ground;
|
||||||
struct event ground_timer;
|
struct event ground_timer;
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
void *kitty_state;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Helper functions. */
|
/* Helper functions. */
|
||||||
@@ -913,6 +916,9 @@ input_free(struct input_ctx *ictx)
|
|||||||
event_del(&ictx->request_timer);
|
event_del(&ictx->request_timer);
|
||||||
|
|
||||||
free(ictx->input_buf);
|
free(ictx->input_buf);
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
kitty_free_state(ictx->kitty_state);
|
||||||
|
#endif
|
||||||
evbuffer_free(ictx->since_ground);
|
evbuffer_free(ictx->since_ground);
|
||||||
event_del(&ictx->ground_timer);
|
event_del(&ictx->ground_timer);
|
||||||
|
|
||||||
@@ -1172,7 +1178,7 @@ input_send_reply(struct input_ctx *ictx, const char *reply)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Reply to terminal query. */
|
/* Reply to terminal query. */
|
||||||
static void printflike(3, 4)
|
void printflike(3, 4)
|
||||||
input_reply(struct input_ctx *ictx, int add, const char *fmt, ...)
|
input_reply(struct input_ctx *ictx, int add, const char *fmt, ...)
|
||||||
{
|
{
|
||||||
struct input_request *ir;
|
struct input_request *ir;
|
||||||
@@ -2803,6 +2809,14 @@ input_exit_apc(struct input_ctx *ictx)
|
|||||||
return;
|
return;
|
||||||
log_debug("%s: \"%s\" (size %zu)", __func__, buf, len);
|
log_debug("%s: \"%s\" (size %zu)", __func__, buf, len);
|
||||||
|
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
if (wp != NULL && len > 1 && buf[0] == 'G') {
|
||||||
|
kitty_parse_input(ictx, wp, sctx, ictx->cell.cell.bg,
|
||||||
|
&ictx->kitty_state, buf, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (wp != NULL &&
|
if (wp != NULL &&
|
||||||
options_get_number(wp->options, "allow-set-title") &&
|
options_get_number(wp->options, "allow-set-title") &&
|
||||||
screen_set_title(sctx->s, buf, 1)) {
|
screen_set_title(sctx->s, buf, 1)) {
|
||||||
|
|||||||
127
regress/image-kitty-decode.sh
Executable file
127
regress/image-kitty-decode.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Check RGB, RGBA and PNG pixels, including compression and transparency.
|
||||||
|
# These tests require libpng and zlib in the tmux build.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import sys
|
||||||
|
import zlib
|
||||||
|
from image_test import image_test, kitty, png
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
def rows(data, width, height, channels):
|
||||||
|
"""Split raster bytes into PNG rows."""
|
||||||
|
size = width * channels
|
||||||
|
return [data[y * size:(y + 1) * size] for y in range(height)]
|
||||||
|
|
||||||
|
|
||||||
|
def check(test, client, command, expected):
|
||||||
|
"""Check newly decoded pixels and their later redraw."""
|
||||||
|
test.emit(b'\033[2J\033[H' + command)
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
|
||||||
|
|
||||||
|
colours = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)]
|
||||||
|
w, h = 16, 32
|
||||||
|
rgb = [colours[2 * (y >= 16) + (x >= 8)]
|
||||||
|
for y in range(h) for x in range(w)]
|
||||||
|
expected = {(x, y): rgb[y * w + x] for y in range(h) for x in range(w)}
|
||||||
|
raw_rgb = bytes(c for pixel in rgb for c in pixel)
|
||||||
|
raw_rgba = bytes(c for pixel in rgb for c in (*pixel, 255))
|
||||||
|
png_rgb = png(w, h, rows(raw_rgb, w, h, 3), colour_type=2)
|
||||||
|
png_rgba = png(w, h, rows(raw_rgba, w, h, 4))
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
test.run('set-option', '-g', 'scroll-on-clear', 'off')
|
||||||
|
client = test.attach()
|
||||||
|
for format, data in [(24, raw_rgb), (32, raw_rgba),
|
||||||
|
(100, png_rgb), (100, png_rgba)]:
|
||||||
|
for compressed in (False, True):
|
||||||
|
control = 'a=T,i=7,C=1,f=%d' % format
|
||||||
|
if format != 100:
|
||||||
|
control += ',s=16,v=32'
|
||||||
|
payload = data
|
||||||
|
if compressed:
|
||||||
|
control += ',o=z,S=%d' % len(data)
|
||||||
|
payload = zlib.compress(data)
|
||||||
|
try:
|
||||||
|
check(test, client, kitty(control, payload), expected)
|
||||||
|
except AssertionError as e:
|
||||||
|
raise AssertionError((format, compressed)) from e
|
||||||
|
|
||||||
|
# Palette PNGs retain their colours and transparent palette entries.
|
||||||
|
indexes = bytes(2 * (y >= 16) + (x >= 8)
|
||||||
|
for y in range(h) for x in range(w))
|
||||||
|
palette = bytes(c for pixel in colours for c in pixel)
|
||||||
|
data = png(w, h, rows(indexes, w, h, 1), colour_type=3,
|
||||||
|
chunks=[(b'PLTE', palette), (b'tRNS', b'\xff\0\xff\xff')])
|
||||||
|
visible = {point: colour for point, colour in expected.items()
|
||||||
|
if not (point[0] >= 8 and point[1] < 16)}
|
||||||
|
check(test, client, kitty('a=T,i=7,f=100,C=1', data), visible)
|
||||||
|
|
||||||
|
# Grayscale and grayscale-alpha PNGs expand to the shared RGBA format.
|
||||||
|
gray = bytes(255 if y >= 16 else 0 for y in range(h) for x in range(w))
|
||||||
|
gray_expected = {(x, y): (gray[y * w + x],) * 3
|
||||||
|
for y in range(h) for x in range(w)}
|
||||||
|
data = png(w, h, rows(gray, w, h, 1), colour_type=0)
|
||||||
|
check(test, client, kitty('a=T,i=7,f=100,C=1', data), gray_expected)
|
||||||
|
gray_alpha = bytes(c for y in range(h) for x in range(w)
|
||||||
|
for c in (gray[y * w + x], 255 if x < 8 else 0))
|
||||||
|
data = png(w, h, rows(gray_alpha, w, h, 2), colour_type=4)
|
||||||
|
check(test, client, kitty('a=T,i=7,f=100,C=1', data),
|
||||||
|
{point: colour for point, colour in gray_expected.items()
|
||||||
|
if point[0] < 8})
|
||||||
|
|
||||||
|
# Test both an entirely transparent cell and holes inside a visible cell.
|
||||||
|
rgba = bytes(c for y in range(h) for x in range(w)
|
||||||
|
for c in (*rgb[y * w + x], 0 if (x >= 8 and y < 16) or
|
||||||
|
(x + y) % 3 == 0 else 255))
|
||||||
|
visible = {point: colour for point, colour in expected.items()
|
||||||
|
if rgba[(point[1] * w + point[0]) * 4 + 3] != 0}
|
||||||
|
for format, data in [(32, rgba), (100, png(w, h, rows(rgba, w, h, 4)))]:
|
||||||
|
control = 'a=T,i=7,f=%d,C=1' % format
|
||||||
|
if format == 32:
|
||||||
|
control += ',s=16,v=32'
|
||||||
|
check(test, client, kitty(control, data), visible)
|
||||||
|
empty = b'\xff\0\0\0' * (w * h)
|
||||||
|
data = empty if format == 32 else png(w, h, rows(empty, w, h, 4))
|
||||||
|
check(test, client, kitty(control, data), {})
|
||||||
|
|
||||||
|
# SIXEL represents partial alpha as a mixture of coloured and empty pixels.
|
||||||
|
test.run('set-option', '-as', 'terminal-features', 'screen-256color:RGB')
|
||||||
|
fallback = test.attach(graphical=False)
|
||||||
|
rgba = b'\xff\0\0\x40' * 64 + b'\xff\0\0\xc0' * 64
|
||||||
|
for format, data in [(32, rgba), (100, png(8, 16, rows(rgba, 8, 16, 4)))]:
|
||||||
|
control = 'a=T,i=7,f=%d,C=1' % format
|
||||||
|
if format == 32:
|
||||||
|
control += ',s=8,v=16'
|
||||||
|
test.emit(b'\033[2J\033[H' + kitty(control, data))
|
||||||
|
for redraw in (False, True):
|
||||||
|
if redraw:
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
upper = sum(y < 8 for x, y in terminal.pixels)
|
||||||
|
lower = len(terminal.pixels) - upper
|
||||||
|
assert 12 <= upper <= 20 and 44 <= lower <= 52, (upper, lower)
|
||||||
|
assert all(colour == (255, 0, 0)
|
||||||
|
for colour in terminal.pixels.values()), terminal.pixels
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(fallback.data)
|
||||||
|
char, fg, bg, bold = terminal.cells[0, 0]
|
||||||
|
assert char == '\u2584' and not bold, (char, bold)
|
||||||
|
assert (fg, bg) == ((192, 0, 0), (64, 0, 0)), (fg, bg)
|
||||||
|
PY
|
||||||
148
regress/image-kitty-delete.sh
Executable file
148
regress/image-kitty-delete.sh
Executable file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Kitty deletion selects placements and preserves other images and text.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import sys
|
||||||
|
from image_test import image_test, kitty, sixel
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
def rectangle(colour, xoff=0, yoff=0):
|
||||||
|
"""Expected pixels for a one-column, two-row placement."""
|
||||||
|
return {(x + xoff, y + yoff): colour for y in range(32) for x in range(8)}
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(test, client, expected):
|
||||||
|
"""Check image positions and colours after a full redraw."""
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
return terminal
|
||||||
|
|
||||||
|
|
||||||
|
red, green, blue, white = (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)
|
||||||
|
uploads = kitty('a=t,i=7,f=32,s=1,v=1', bytes((*red, 255))) + \
|
||||||
|
kitty('a=t,i=8,f=32,s=1,v=1', bytes((*green, 255)))
|
||||||
|
put = 'a=p,i=7,C=1,c=1,r=2'
|
||||||
|
display = b'\033[H' + kitty(put + ',p=1') + b'\033[1;3H' + \
|
||||||
|
kitty(put + ',p=2') + b'\033[1;5H' + \
|
||||||
|
kitty('a=p,i=8,p=1,C=1,c=1,r=2') + b'\033[1;7H' + \
|
||||||
|
kitty('a=T,f=32,s=1,v=1,C=1,c=1,r=2', bytes((*blue, 255))) + \
|
||||||
|
b'\033[1;9H' + sixel(8, 32, [(100, 100, 100)], [0] * 256) + b'\033[2;3H'
|
||||||
|
pictures = dict(first=rectangle(red), second=rectangle(red, 16),
|
||||||
|
other=rectangle(green, 32), anonymous=rectangle(blue, 48),
|
||||||
|
sixel=rectangle(white, 64))
|
||||||
|
cases = [
|
||||||
|
('one placement', kitty('a=d,d=i,i=7,p=1'),
|
||||||
|
['second', 'other', 'anonymous', 'sixel']),
|
||||||
|
('one image', kitty('a=d,d=i,i=7'), ['other', 'anonymous', 'sixel']),
|
||||||
|
('placement zero', kitty('a=d,d=i,i=7,p=0'),
|
||||||
|
['other', 'anonymous', 'sixel']),
|
||||||
|
('placement IDs belong to each image', kitty('a=d,d=i,i=8,p=1'),
|
||||||
|
['first', 'second', 'anonymous', 'sixel']),
|
||||||
|
('missing image', kitty('a=d,d=I,i=999'), list(pictures)),
|
||||||
|
('missing placement', kitty('a=d,d=I,i=7,p=999'), list(pictures)),
|
||||||
|
('missing image ID', kitty('a=d,d=I'), list(pictures)),
|
||||||
|
('all images', kitty('a=d,d=a'), ['sixel']),
|
||||||
|
('default selector', kitty('a=d'), ['sixel']),
|
||||||
|
('all ignores image and placement IDs', kitty('a=d,d=A,i=7,p=1'),
|
||||||
|
['sixel']),
|
||||||
|
('repeated deletion', kitty('a=d,d=I,i=7,p=1') * 2,
|
||||||
|
['second', 'other', 'anonymous', 'sixel']),
|
||||||
|
('selector resets', kitty('a=d,d=i,i=7') + kitty('a=d'), ['sixel']),
|
||||||
|
]
|
||||||
|
for selector in 'cCpPqQrRxXyYzZ':
|
||||||
|
cases.append(('unsupported selector ' + selector,
|
||||||
|
kitty('a=d,d=%s,i=7,p=1,x=1,y=2,z=0' % selector), list(pictures)))
|
||||||
|
for z in (-2147483648, -1, 1, 2147483647):
|
||||||
|
cases.append(('unsupported layer ' + str(z),
|
||||||
|
kitty('a=d,d=I,i=7,p=1,z=%d' % z), list(pictures)))
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
test.run('set-option', '-g', 'scroll-on-clear', 'off')
|
||||||
|
client = test.attach()
|
||||||
|
fallback = test.attach(graphical=False)
|
||||||
|
for name, command, retained in cases:
|
||||||
|
test.emit(b'\033[2J' + uploads + display)
|
||||||
|
original = {point: colour for image in pictures.values()
|
||||||
|
for point, colour in image.items()}
|
||||||
|
terminal = snapshot(test, client, original)
|
||||||
|
cursor = test.run('display-message', '-p', '#{cursor_x},#{cursor_y}')
|
||||||
|
test.emit(command)
|
||||||
|
wanted = {point: colour for name in retained
|
||||||
|
for point, colour in pictures[name].items()}
|
||||||
|
try:
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(wanted)
|
||||||
|
snapshot(test, client, wanted)
|
||||||
|
assert test.run('display-message', '-p',
|
||||||
|
'#{cursor_x},#{cursor_y}') == cursor
|
||||||
|
assert not fallback.images(), 'graphics sent to fallback client'
|
||||||
|
except AssertionError as e:
|
||||||
|
raise AssertionError(name) from e
|
||||||
|
|
||||||
|
# Deleting the upper placement reveals the lower one.
|
||||||
|
test.emit(b'\033[2J\033[H' + uploads + kitty(put + ',p=1') +
|
||||||
|
kitty('a=p,i=8,p=1,C=1,c=1,r=2') + b'\033[1;3H' + kitty(put + ',p=2'))
|
||||||
|
terminal = snapshot(test, client, rectangle(green) | rectangle(red, 16))
|
||||||
|
test.emit(kitty('a=d,d=I,i=8,p=1'))
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(rectangle(red) | rectangle(red, 16))
|
||||||
|
snapshot(test, client, rectangle(red) | rectangle(red, 16))
|
||||||
|
test.emit(kitty('a=d,d=A'))
|
||||||
|
snapshot(test, client, {})
|
||||||
|
|
||||||
|
# Deleting an image restores the text which was updated beneath it.
|
||||||
|
test.emit(b'\033[2J\033[H' + uploads + kitty(put + ',p=1') + b'\033[HX')
|
||||||
|
assert test.run('capture-pane', '-p') == 'X'
|
||||||
|
test.emit(kitty('a=d,d=I,i=7'))
|
||||||
|
snapshot(test, client, {})
|
||||||
|
for attached in [client, fallback]:
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(attached.data)
|
||||||
|
assert terminal.cells[0, 0][0] == 'X', terminal.cells[0, 0]
|
||||||
|
|
||||||
|
# A visible part selects its whole placement, including the part in history.
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(uploads + b'\033[H' + kitty(put + ',p=1') + b'\033[S')
|
||||||
|
snapshot(test, client, {point: colour
|
||||||
|
for point, colour in rectangle(red, yoff=-16).items() if point[1] >= 0})
|
||||||
|
test.emit(kitty('a=d,d=A'))
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.run('copy-mode')
|
||||||
|
test.run('send-keys', '-X', 'history-top')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.run('send-keys', '-X', 'cancel')
|
||||||
|
|
||||||
|
# Deletion on the alternate screen leaves the saved primary placements intact.
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(uploads + b'\033[H' + kitty(put + ',p=1') + b'\033[?1049h\033[H' +
|
||||||
|
kitty(put + ',p=2'))
|
||||||
|
snapshot(test, client, rectangle(red))
|
||||||
|
test.emit(kitty('a=d,d=I,i=7'))
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.emit(b'\033[?1049l')
|
||||||
|
snapshot(test, client, rectangle(red))
|
||||||
|
test.emit(b'\033[1;3H' + kitty(put + ',p=2'))
|
||||||
|
snapshot(test, client, rectangle(red) | rectangle(red, 16))
|
||||||
|
|
||||||
|
# Deletion by image ID also removes placements entirely in history.
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(uploads + b'\033[H' + kitty(put + ',p=1') + b'\033[16S')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.emit(kitty('a=d,d=I,i=7'))
|
||||||
|
test.run('copy-mode')
|
||||||
|
test.run('send-keys', '-X', 'history-top')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.run('send-keys', '-X', 'cancel')
|
||||||
|
PY
|
||||||
135
regress/image-kitty-layers.sh
Normal file
135
regress/image-kitty-layers.sh
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Image ordering and overlapping Kitty placements through SIXEL and fallback.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import sys
|
||||||
|
from image_test import image_test, kitty, sixel
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
red, green, blue, white = (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
|
def upload(id, colour):
|
||||||
|
"""Upload a solid pixel under a Kitty image ID."""
|
||||||
|
return kitty('a=t,i=%d,f=32,s=1,v=1' % id, bytes((*colour, 255)))
|
||||||
|
|
||||||
|
|
||||||
|
def put(id, controls=''):
|
||||||
|
"""Place a stored image in two columns without moving the cursor."""
|
||||||
|
return kitty('a=p,i=%d,C=1,c=2,r=1' % id + controls)
|
||||||
|
|
||||||
|
|
||||||
|
def rectangle(colour, xoff=0, yoff=0, w=16, h=16):
|
||||||
|
"""Expected image pixels for a solid rectangle."""
|
||||||
|
return {(x + xoff, y + yoff): colour for y in range(h) for x in range(w)}
|
||||||
|
|
||||||
|
|
||||||
|
def check(test, client, terminal, expected):
|
||||||
|
"""Check incremental output and a full redraw of the same scene."""
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
return terminal
|
||||||
|
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
test.run('set-option', '-g', 'scroll-on-clear', 'off')
|
||||||
|
client = test.attach()
|
||||||
|
fallback = test.attach(graphical=False)
|
||||||
|
terminal = Terminal()
|
||||||
|
|
||||||
|
# The PR's tie-breaker cases: upload order wins over placement and app IDs.
|
||||||
|
test.emit(upload(200, red) + upload(100, white) + put(100))
|
||||||
|
terminal = check(test, client, terminal, rectangle(white))
|
||||||
|
test.emit(put(200))
|
||||||
|
terminal = check(test, client, terminal, rectangle(white))
|
||||||
|
# Replacing the older source preserves its place in the drawing order.
|
||||||
|
test.emit(upload(200, green) + put(200))
|
||||||
|
terminal = check(test, client, terminal, rectangle(white))
|
||||||
|
test.emit(kitty('a=d,d=I,i=200') + upload(200, red) + put(200))
|
||||||
|
terminal = check(test, client, terminal, rectangle(red))
|
||||||
|
# A retransmit-and-place also inherits the old source's drawing order.
|
||||||
|
test.emit(b'\033[2J\033[H' + upload(200, red) + upload(100, white) +
|
||||||
|
put(200) + put(100) + kitty('a=T,i=100,f=32,s=1,v=1,C=1,c=2,r=1',
|
||||||
|
bytes((*green, 255))))
|
||||||
|
# 200 was recreated above, so it is newer than 100 despite retransmission.
|
||||||
|
terminal = check(test, client, terminal, rectangle(red))
|
||||||
|
|
||||||
|
# Placements of the same source use placement order, including after reflow.
|
||||||
|
pixels = bytes((*red, 255, *blue, 255))
|
||||||
|
test.emit(b'\033[2J\033[H' + kitty('a=t,i=300,f=32,s=2,v=1', pixels) +
|
||||||
|
put(300, ',p=99,x=1,w=1') + put(300, ',p=1,x=0,w=1'))
|
||||||
|
terminal = check(test, client, terminal, rectangle(red))
|
||||||
|
test.run('resize-window', '-x', '10')
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(rectangle(red))
|
||||||
|
test.run('resize-window', '-x', '40')
|
||||||
|
|
||||||
|
# Replacing a named placement puts it after the other source placements.
|
||||||
|
test.emit(put(300, ',p=99,x=1,w=1'))
|
||||||
|
terminal = check(test, client, terminal, rectangle(blue))
|
||||||
|
test.emit(kitty('a=d,d=i,i=300,p=99'))
|
||||||
|
terminal = check(test, client, terminal, rectangle(red))
|
||||||
|
|
||||||
|
# Both protocols use source creation order, even when placed later.
|
||||||
|
graphics = sixel(16, 16, [(100, 100, 100)], [0] * 256)
|
||||||
|
test.emit(b'\033[2J\033[H' + upload(400, red) + graphics +
|
||||||
|
b'\033[H' + put(400))
|
||||||
|
terminal = check(test, client, Terminal(), rectangle(white))
|
||||||
|
# Replacing the older Kitty source leaves the newer SIXEL on top.
|
||||||
|
test.emit(upload(400, green) + put(400))
|
||||||
|
terminal = check(test, client, terminal, rectangle(white))
|
||||||
|
test.emit(b'\033[2J\033[H' + graphics + b'\033[H' +
|
||||||
|
upload(401, red) + put(401))
|
||||||
|
terminal = check(test, client, Terminal(), rectangle(red))
|
||||||
|
term = Terminal()
|
||||||
|
term.feed(fallback.data)
|
||||||
|
assert term.cells[0, 0][1:3] == (196, 196), term.cells[0, 0]
|
||||||
|
# Removing the newer Kitty placement reveals the retained SIXEL image.
|
||||||
|
test.emit(kitty('a=d,d=I,i=401'))
|
||||||
|
terminal = check(test, client, terminal, rectangle(white))
|
||||||
|
term = Terminal()
|
||||||
|
term.feed(fallback.data)
|
||||||
|
assert term.cells[0, 0][1:3] == (231, 231), term.cells[0, 0]
|
||||||
|
|
||||||
|
# Transparency preserves the lower image across redraws and deletion.
|
||||||
|
pixels = bytes(value for y in range(16) for x in range(16)
|
||||||
|
for value in (*blue, 255 if x < 8 else 0))
|
||||||
|
for base, colour in [(upload(500, red) + put(500), red), (graphics, white)]:
|
||||||
|
test.emit(b'\033[2J\033[H' + base + b'\033[H' +
|
||||||
|
kitty('a=T,i=501,f=32,s=16,v=16,C=1', pixels))
|
||||||
|
terminal = check(test, client, Terminal(),
|
||||||
|
rectangle(blue, w=8) | rectangle(colour, 8, w=8))
|
||||||
|
test.emit(kitty('a=d,d=I,i=501'))
|
||||||
|
terminal = check(test, client, terminal, rectangle(colour))
|
||||||
|
|
||||||
|
# Rejected layers cannot replace stored pixels or remove placements.
|
||||||
|
test.emit(b'\033[2J\033[H' + upload(600, red) + put(600))
|
||||||
|
terminal = check(test, client, Terminal(), rectangle(red))
|
||||||
|
for action in ('t', 'T', 'p', 'q', 'd'):
|
||||||
|
test.emit(kitty('a=%s,i=600,p=9,f=32,s=1,v=1,C=1,d=I,z=1' %
|
||||||
|
action, bytes((*green, 255))))
|
||||||
|
terminal = check(test, client, terminal, rectangle(red))
|
||||||
|
test.emit(kitty('a=p,i=600,p=9,C=1,c=2,r=1,z=0'))
|
||||||
|
terminal = check(test, client, terminal, rectangle(red))
|
||||||
|
|
||||||
|
# Images at the default layer cover text and explicit backgrounds.
|
||||||
|
test.emit(b'\033[2J\033[H\033[44mX \033[0m\033[H' + put(600))
|
||||||
|
check(test, client, Terminal(), rectangle(red))
|
||||||
|
term = Terminal()
|
||||||
|
term.feed(fallback.data)
|
||||||
|
assert term.cells[0, 0][1:3] == (196, 196), term.cells[0, 0]
|
||||||
|
PY
|
||||||
109
regress/image-kitty-lifecycle.sh
Executable file
109
regress/image-kitty-lifecycle.sh
Executable file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Kitty placement recovery, reflow, alternate screens and pane-local IDs.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import sys
|
||||||
|
from image_test import image_test, kitty
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(test, client, expected, sx=40, sy=16):
|
||||||
|
"""Check visible image pixels after an independent redraw."""
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal(sx=sx, sy=sy)
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
|
||||||
|
|
||||||
|
red, green, blue = (255, 0, 0), (0, 255, 0), (0, 0, 255)
|
||||||
|
raw = bytes(c for y in range(32) for x in range(32)
|
||||||
|
for c in (*(red if x < 16 else blue), 255))
|
||||||
|
upload = kitty('a=t,i=7,f=32,s=32,v=32', raw)
|
||||||
|
place = kitty('a=p,i=7,p=9,C=1')
|
||||||
|
expected = {(x, y): red if x < 16 else blue
|
||||||
|
for y in range(32) for x in range(32)}
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(upload + place)
|
||||||
|
for width in (1, 40, 2, 40):
|
||||||
|
test.resize(client, width, 16)
|
||||||
|
snapshot(test, client,
|
||||||
|
{point: colour for point, colour in expected.items()
|
||||||
|
if point[0] < width * 8}, sx=width)
|
||||||
|
# Recovering a clipped named placement must still allow its replacement.
|
||||||
|
test.emit(b'\033[4;5H' + place)
|
||||||
|
snapshot(test, client,
|
||||||
|
{(x + 32, y + 48): colour for (x, y), colour in expected.items()})
|
||||||
|
|
||||||
|
# An alternate screen has its own grid, while the original grid is retained.
|
||||||
|
test.emit(b'\033[?1049h')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.emit(b'\033[H' + kitty('a=T,i=8,p=9,f=32,s=1,v=1,C=1,c=1,r=1',
|
||||||
|
bytes((*green, 255))))
|
||||||
|
snapshot(test, client,
|
||||||
|
{(x, y): green for y in range(16) for x in range(8)})
|
||||||
|
test.emit(b'\033[?1049l')
|
||||||
|
snapshot(test, client,
|
||||||
|
{(x + 32, y + 48): colour for (x, y), colour in expected.items()})
|
||||||
|
|
||||||
|
# Kitty image rows retain their width while adjacent wrapped text reflows.
|
||||||
|
with image_test(sys.argv[1], sx=16, sy=20) as test:
|
||||||
|
client = test.attach()
|
||||||
|
text = b'abcdefghijklmnopqrstuvwx'
|
||||||
|
test.emit(text + b'\r\n' + upload + kitty('a=p,i=7,p=9') +
|
||||||
|
b'\r\n' + text + b'\r\n')
|
||||||
|
for width in (8, 16, 8, 16):
|
||||||
|
test.resize(client, width, 20)
|
||||||
|
top = (len(text) + width - 1) // width - int(test.run(
|
||||||
|
'display-message', '-p', '#{history_size}'))
|
||||||
|
assert top >= 0, top
|
||||||
|
snapshot(test, client,
|
||||||
|
{(x, y + top * 16): colour for (x, y), colour in expected.items()},
|
||||||
|
sx=width, sy=20)
|
||||||
|
captured = test.run('capture-pane', '-p', '-S', '-').replace('\n', '')
|
||||||
|
assert captured == text.decode() * 2, captured
|
||||||
|
|
||||||
|
# Equal application image and placement IDs in separate panes are independent.
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
first = test.run('display-message', '-p', '#{pane_id}')
|
||||||
|
second = test.run('split-window', '-d', '-P', '-F', '#{pane_id}',
|
||||||
|
"stty raw -echo; printf '\033]2;SECOND\007'; exec cat")
|
||||||
|
test.wait(lambda: test.run('display-message', '-pt', second,
|
||||||
|
'#{pane_title}') == 'SECOND')
|
||||||
|
placed = {}
|
||||||
|
for pane, colour in [(first, red), (second, blue)]:
|
||||||
|
test.run('select-pane', '-t', pane)
|
||||||
|
test.emit(kitty('a=T,i=7,p=9,f=32,s=1,v=1,C=1,c=1,r=1',
|
||||||
|
bytes((*colour, 255))))
|
||||||
|
x, y = map(int, test.run('display-message', '-pt', pane,
|
||||||
|
'#{pane_left},#{pane_top}').split(','))
|
||||||
|
placed.update({(x * 8 + px, y * 16 + py): colour
|
||||||
|
for py in range(16) for px in range(8)})
|
||||||
|
snapshot(test, client, placed)
|
||||||
|
test.run('select-pane', '-t', first)
|
||||||
|
test.emit(kitty('a=T,i=7,p=9,f=32,s=1,v=1,C=1,c=1,r=1',
|
||||||
|
bytes((*green, 255))))
|
||||||
|
placed = {point: green if colour == red else colour
|
||||||
|
for point, colour in placed.items()}
|
||||||
|
snapshot(test, client, placed)
|
||||||
|
test.run('kill-pane', '-t', second)
|
||||||
|
snapshot(test, client,
|
||||||
|
{(x, y): green for y in range(16) for x in range(8)})
|
||||||
|
# Respawning removes the previous application's placements and sources.
|
||||||
|
test.run('respawn-pane', '-k', '-t', first,
|
||||||
|
"stty raw -echo; printf '\033]2;RESPAWNED\007'; exec cat")
|
||||||
|
test.wait(lambda: test.run('display-message', '-p',
|
||||||
|
'#{pane_title}') == 'RESPAWNED')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.emit(kitty('a=p,i=7,p=9,C=1'))
|
||||||
|
snapshot(test, client, {})
|
||||||
|
PY
|
||||||
207
regress/image-kitty-persistence.sh
Executable file
207
regress/image-kitty-persistence.sh
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Kitty placements survive text writes and erases, but follow screen clears
|
||||||
|
# and scrolling. Overwritten SIXEL pixels can reveal an older Kitty image.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import base64
|
||||||
|
import sys
|
||||||
|
from image_test import image_test, sixel
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
def graphics(control, data=b''):
|
||||||
|
"""Build a Kitty graphics APC with replies suppressed."""
|
||||||
|
payload = b';' + base64.b64encode(data) if data else b''
|
||||||
|
return b'\033_Gq=2,' + control.encode() + payload + b'\033\\'
|
||||||
|
|
||||||
|
|
||||||
|
red, blue = (255, 0, 0), (0, 0, 255)
|
||||||
|
origin = b'\033[3;3H'
|
||||||
|
upload = graphics('a=t,i=7,f=32,s=1,v=1', bytes((*red, 255)))
|
||||||
|
place = origin + graphics('a=p,i=7,p=9,c=4,r=3,C=1')
|
||||||
|
cells = {(x, y): red for y in range(2, 5) for x in range(2, 6)}
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(test, client, expected):
|
||||||
|
"""Check image pixels after a full redraw."""
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
return terminal
|
||||||
|
|
||||||
|
|
||||||
|
def pixels(cells):
|
||||||
|
"""Expand solid image cells into positioned pixels."""
|
||||||
|
return {(x * 8 + px, y * 16 + py): colour
|
||||||
|
for (x, y), colour in cells.items()
|
||||||
|
for py in range(16) for px in range(8)}
|
||||||
|
|
||||||
|
|
||||||
|
cases = [
|
||||||
|
('ASCII text', origin + b'ab'),
|
||||||
|
('wide text', origin + '\u754c'.encode()),
|
||||||
|
('insert characters', origin + b'\033[2@'),
|
||||||
|
('delete characters', b'\033[3;4H\033[2P'),
|
||||||
|
('insert mode', origin + b'\033[4hX\033[4l'),
|
||||||
|
('wide insert mode', origin + b'\033[4h' +
|
||||||
|
'\u754c'.encode() + b'\033[4l'),
|
||||||
|
('insert line', b'\033[4;1H\033[L'),
|
||||||
|
('delete line', b'\033[4;1H\033[M'),
|
||||||
|
('insert lines', b'\033[4;1H\033[2L'),
|
||||||
|
('delete lines', b'\033[4;1H\033[2M'),
|
||||||
|
('insert all remaining lines', b'\033[4;1H\033[99L'),
|
||||||
|
('line edit outside margins', b'\033[6;12r\033[4;1H\033[L\033[r'),
|
||||||
|
('erase characters', b'\033[3;4H\033[2X'),
|
||||||
|
('erase line end', b'\033[4;5H\033[K'),
|
||||||
|
('erase line start', b'\033[4;4H\033[1K'),
|
||||||
|
('erase whole line', b'\033[4;4H\033[2K'),
|
||||||
|
('erase display end', b'\033[4;5H\033[J'),
|
||||||
|
('erase display start', b'\033[4;5H\033[1J'),
|
||||||
|
('erase whole display', b'\033[2J'),
|
||||||
|
]
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
test.run('set-option', '-g', 'scroll-on-clear', 'off')
|
||||||
|
client = test.attach()
|
||||||
|
fallback = test.attach(graphical=False)
|
||||||
|
test.emit(upload)
|
||||||
|
for name, operation in cases:
|
||||||
|
test.emit(b'\033[2J' + place)
|
||||||
|
wanted = pixels(cells)
|
||||||
|
terminal = snapshot(test, client, wanted)
|
||||||
|
test.emit(operation)
|
||||||
|
if name == 'erase whole display':
|
||||||
|
wanted = {}
|
||||||
|
try:
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(wanted)
|
||||||
|
snapshot(test, client, wanted)
|
||||||
|
# Fallback must still show the retained image after a line erase.
|
||||||
|
term = Terminal()
|
||||||
|
term.feed(fallback.data)
|
||||||
|
for x, y in cells:
|
||||||
|
assert term.cells[x, y][2] == (196 if wanted else None), (
|
||||||
|
x, y, term.cells[x, y])
|
||||||
|
except AssertionError as e:
|
||||||
|
raise AssertionError(name) from e
|
||||||
|
|
||||||
|
# Image persistence must not prevent writes and erases of underlying text.
|
||||||
|
test.emit(b'\033[2J' + place + origin + b'ab')
|
||||||
|
assert test.run('capture-pane', '-p') == 'ab'
|
||||||
|
test.emit(origin + b'\033[X')
|
||||||
|
assert test.run('capture-pane', '-p') == 'b'
|
||||||
|
snapshot(test, client, pixels(cells))
|
||||||
|
test.emit(origin + '\u754c'.encode())
|
||||||
|
assert test.run('capture-pane', '-p') == '\u754c'
|
||||||
|
test.emit(origin + b'\033[2K')
|
||||||
|
assert test.run('capture-pane', '-p') == ''
|
||||||
|
snapshot(test, client, pixels(cells))
|
||||||
|
|
||||||
|
# Text line edits preserve images; complete images follow scrolling.
|
||||||
|
colours = [red, (0, 255, 0), blue]
|
||||||
|
striped = {(x, y): colours[y - 2] for x, y in cells}
|
||||||
|
data = bytes(c for colour in colours for c in (*colour, 255))
|
||||||
|
display = origin + graphics('a=T,i=8,p=9,f=32,s=1,v=3,c=4,r=3,C=1', data)
|
||||||
|
operations = [
|
||||||
|
('insert line', b'\033[4;1H\033[L', striped),
|
||||||
|
('delete line', b'\033[4;1H\033[M', striped),
|
||||||
|
('scroll down', b'\033[3;6r\033[T',
|
||||||
|
{(x, y + 1): c for (x, y), c in striped.items()}),
|
||||||
|
('reverse index', b'\033[3;6r\033[3;1H\033M',
|
||||||
|
{(x, y + 1): c for (x, y), c in striped.items()}),
|
||||||
|
]
|
||||||
|
for name, operation, wanted in operations:
|
||||||
|
test.emit(b'\033[r\033[2J' + display + operation)
|
||||||
|
try:
|
||||||
|
snapshot(test, client, pixels(wanted))
|
||||||
|
except AssertionError as e:
|
||||||
|
raise AssertionError(name) from e
|
||||||
|
test.emit(b'\033[r')
|
||||||
|
|
||||||
|
# Erasing a SIXEL layer reveals the Kitty placement retained beneath it.
|
||||||
|
wire = sixel(32, 48, [(0, 0, 100)], [0] * (32 * 48))
|
||||||
|
test.emit(b'\033[2J' + place + origin + wire)
|
||||||
|
terminal = snapshot(test, client,
|
||||||
|
pixels({point: blue for point in cells}))
|
||||||
|
test.emit(b'\033[4;1H\033[2K')
|
||||||
|
wanted = pixels({(x, y): red if y == 3 else blue for x, y in cells})
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(wanted)
|
||||||
|
snapshot(test, client, wanted)
|
||||||
|
|
||||||
|
# An image crossing a scroll margin stays intact and stationary.
|
||||||
|
test.emit(b'\033[2J' + place + b'\033[4;7r\033[S')
|
||||||
|
wanted = pixels(cells)
|
||||||
|
snapshot(test, client, wanted)
|
||||||
|
test.emit(b'\033[r')
|
||||||
|
|
||||||
|
# Complete images scroll and clip; images crossing either margin stay.
|
||||||
|
operations = [
|
||||||
|
('clip all', b'\033[3;6r\033[3S', {}),
|
||||||
|
('clip top', b'\033[3;6r\033[S',
|
||||||
|
{(x, y - 1): c for (x, y), c in striped.items() if y > 2}),
|
||||||
|
('clip bottom', b'\033[3;5r\033[T',
|
||||||
|
{(x, y + 1): c for (x, y), c in striped.items() if y < 4}),
|
||||||
|
('cross top', b'\033[4;7r\033[S', striped),
|
||||||
|
('cross bottom', b'\033[1;4r\033[T', striped),
|
||||||
|
('cross top reverse index', b'\033[4;7r\033[4;1H\033M', striped),
|
||||||
|
]
|
||||||
|
for name, operation, wanted in operations:
|
||||||
|
test.emit(b'\033[r\033[2J' + display + operation)
|
||||||
|
try:
|
||||||
|
snapshot(test, client, pixels(wanted))
|
||||||
|
except AssertionError as e:
|
||||||
|
raise AssertionError(name) from e
|
||||||
|
test.emit(b'\033[r')
|
||||||
|
|
||||||
|
# Two placements of the same image can scroll differently at a margin.
|
||||||
|
other = b'\033[5;9H' + graphics(
|
||||||
|
'a=p,i=8,p=10,c=4,r=3,C=1')
|
||||||
|
test.emit(b'\033[2J' + display + other + b'\033[4;7r\033[S\033[r')
|
||||||
|
wanted = striped | {(x + 6, y + 1): c for (x, y), c in striped.items()}
|
||||||
|
snapshot(test, client, pixels(wanted))
|
||||||
|
|
||||||
|
# With scroll-on-clear enabled, Kitty images move into history.
|
||||||
|
test.emit(b'\033[2J')
|
||||||
|
test.run('clear-history')
|
||||||
|
test.run('set-option', '-g', 'scroll-on-clear', 'on')
|
||||||
|
test.emit(place + b'\033[2J')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
assert int(test.run('display-message', '-p', '#{history_size}')) > 0
|
||||||
|
# Scrolling a region must leave images already in history in place.
|
||||||
|
test.emit(b'\033[4;7r\033[S\033[r')
|
||||||
|
test.run('copy-mode')
|
||||||
|
test.run('send-keys', '-X', 'history-top')
|
||||||
|
snapshot(test, client, pixels(cells))
|
||||||
|
test.run('send-keys', '-X', 'cancel')
|
||||||
|
# Replacing a source removes its placements even when wholly in history.
|
||||||
|
test.emit(graphics('a=t,i=7,f=32,s=1,v=1', bytes((*blue, 255))))
|
||||||
|
test.run('copy-mode')
|
||||||
|
test.run('send-keys', '-X', 'history-top')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.run('send-keys', '-X', 'cancel')
|
||||||
|
# Restore a historical placement before checking explicit history cleanup.
|
||||||
|
test.emit(place + b'\033[2J')
|
||||||
|
test.run('clear-history')
|
||||||
|
test.run('copy-mode')
|
||||||
|
test.run('send-keys', '-X', 'history-top')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
|
||||||
|
# The alternate screen has no history, but uses the same image rules.
|
||||||
|
test.run('send-keys', '-X', 'cancel')
|
||||||
|
test.emit(b'\033[?1049h')
|
||||||
|
for name, operation, wanted in operations:
|
||||||
|
test.emit(b'\033[r\033[2J' + display + operation)
|
||||||
|
try:
|
||||||
|
snapshot(test, client, pixels(wanted))
|
||||||
|
except AssertionError as e:
|
||||||
|
raise AssertionError('alternate screen: ' + name) from e
|
||||||
|
test.emit(b'\033[r\033[?1049l')
|
||||||
|
PY
|
||||||
193
regress/image-kitty-placements.sh
Normal file
193
regress/image-kitty-placements.sh
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Kitty ordinary placement geometry, cursor movement and replacement.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import base64
|
||||||
|
import sys
|
||||||
|
from image_test import image_test
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
def graphics(control, data=b''):
|
||||||
|
"""Build a Kitty graphics APC with replies suppressed."""
|
||||||
|
payload = b';' + base64.b64encode(data) if data else b''
|
||||||
|
return b'\033_Gq=2,' + control.encode() + payload + b'\033\\'
|
||||||
|
|
||||||
|
|
||||||
|
colours = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)]
|
||||||
|
raw = bytes(value for y in range(32) for x in range(16)
|
||||||
|
for value in (*colours[2 * (y >= 16) + (x >= 8)], 255))
|
||||||
|
upload = graphics('a=t,i=7,f=32,s=16,v=32', raw)
|
||||||
|
put = 'a=p,i=7,C=1'
|
||||||
|
|
||||||
|
|
||||||
|
def pattern(w=16, h=32, xoff=0, yoff=0):
|
||||||
|
"""Expected positions and colours for a scaled four-quadrant image."""
|
||||||
|
return {(x + xoff, y + yoff): colours[2 * (y >= h // 2) + (x >= w // 2)]
|
||||||
|
for y in range(h) for x in range(w)}
|
||||||
|
|
||||||
|
|
||||||
|
def solid(w, h, colour, xoff=0, yoff=0):
|
||||||
|
"""Expected positions for a solid rectangle."""
|
||||||
|
return {(x + xoff, y + yoff): colour for y in range(h) for x in range(w)}
|
||||||
|
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
fallback = test.attach(graphical=False)
|
||||||
|
|
||||||
|
def check(command, expected, cursor='0,0'):
|
||||||
|
"""Check the displayed image, its redraw and the pane cursor."""
|
||||||
|
test.emit(b'\033[2J\033[H' + command)
|
||||||
|
actual = test.run('display-message', '-p', '#{cursor_x},#{cursor_y}')
|
||||||
|
assert actual == cursor, ('cursor', actual, cursor)
|
||||||
|
term = Terminal()
|
||||||
|
term.feed(client.data)
|
||||||
|
term.check(expected)
|
||||||
|
test.redraw()
|
||||||
|
term = Terminal()
|
||||||
|
term.feed(client.data)
|
||||||
|
term.check(expected)
|
||||||
|
assert not fallback.images(), 'graphics sent to fallback client'
|
||||||
|
|
||||||
|
# Uploading alone draws nothing; placement reuses the stored pixels.
|
||||||
|
check(upload, {})
|
||||||
|
check(graphics(put), pattern())
|
||||||
|
term = Terminal()
|
||||||
|
term.feed(fallback.data)
|
||||||
|
assert term.cells[0, 0][1:3] == (196, 196), term.cells[0, 0]
|
||||||
|
check(graphics('a=T,i=7,f=32,s=16,v=32', raw), pattern(), cursor='2,1')
|
||||||
|
check(graphics(put + ',c=4'), pattern(32, 64))
|
||||||
|
check(graphics(put + ',r=4'), pattern(32, 64))
|
||||||
|
check(graphics(put + ',c=4,r=1'), pattern(32, 16))
|
||||||
|
|
||||||
|
# Crop first, then scale; oversized crop dimensions stop at the source edge.
|
||||||
|
check(graphics(put + ',y=16,w=8,h=16,c=2,r=1'), solid(16, 16, colours[2]))
|
||||||
|
check(graphics(put + ',x=8,y=16,w=999,h=999,c=1,r=1'),
|
||||||
|
solid(8, 16, colours[3]))
|
||||||
|
|
||||||
|
# Natural and explicit sizes both honour offsets within the first cell.
|
||||||
|
check(graphics(put + ',X=4,Y=8'), pattern(xoff=4, yoff=8))
|
||||||
|
check(graphics(put + ',c=3,r=3,X=4,Y=8'), pattern(20, 40, 4, 8))
|
||||||
|
check(graphics(put + ',c=1,r=1,X=4'), pattern(4, 16, 4))
|
||||||
|
check(graphics(put + ',X=4294967295'), pattern(xoff=7))
|
||||||
|
check(b'\033[1;40H' + graphics(put),
|
||||||
|
{(312 + x, y): colours[2 * (y >= 16)]
|
||||||
|
for y in range(32) for x in range(8)}, cursor='39,0')
|
||||||
|
|
||||||
|
# Stationary images clip at the bottom; moving images scroll by one row.
|
||||||
|
check(b'\033[16;1H' + graphics(put),
|
||||||
|
{(x, 240 + y): colours[x >= 8]
|
||||||
|
for y in range(16) for x in range(16)}, cursor='0,15')
|
||||||
|
check(b'\033[16;1H' + graphics('a=p,i=7'), pattern(yoff=224),
|
||||||
|
cursor='2,15')
|
||||||
|
|
||||||
|
# Named placement replacement removes the earlier display.
|
||||||
|
first = graphics(put + ',p=9')
|
||||||
|
second = b'\033[4;5H' + graphics(put + ',p=9,c=1,r=1')
|
||||||
|
check(first + second, pattern(8, 16, 32, 48), cursor='4,3')
|
||||||
|
|
||||||
|
# Placement zero creates independent displays; IDs are local to an image.
|
||||||
|
check(graphics(put) + b'\033[4;5H' + graphics(put),
|
||||||
|
pattern() | pattern(xoff=32, yoff=48), cursor='4,3')
|
||||||
|
blue = graphics('a=t,i=8,f=32,s=1,v=1', b'\0\0\xff\xff')
|
||||||
|
other = b'\033[4;5H' + graphics('a=p,i=8,p=9,C=1,c=1,r=1')
|
||||||
|
check(first + blue + other, pattern() | solid(8, 16, colours[2], 32, 48),
|
||||||
|
cursor='4,3')
|
||||||
|
|
||||||
|
# Failed placement validation preserves the old source and display.
|
||||||
|
bad = graphics('a=T,i=7,p=9,f=32,s=1,v=1,x=1,C=1', b'\0\xff\0\xff')
|
||||||
|
check(first + bad + b'\033[4;5H' + graphics(put + ',c=1,r=1'),
|
||||||
|
pattern() | pattern(8, 16, 32, 48), cursor='4,3')
|
||||||
|
check(first + graphics(put + ',p=9,x=16'), pattern())
|
||||||
|
|
||||||
|
# Successful source replacement removes all its old placements.
|
||||||
|
green = graphics('a=t,i=7,f=32,s=1,v=1', b'\0\xff\0\xff')
|
||||||
|
check(first + b'\033[4;5H' + graphics(put) + green, {}, cursor='4,3')
|
||||||
|
check(graphics(put + ',c=1,r=1'), solid(8, 16, colours[1]))
|
||||||
|
|
||||||
|
# Replacement also removes displays saved by the alternate screen.
|
||||||
|
check(upload + first + b'\033[?1049h' + green + b'\033[?1049l', {})
|
||||||
|
check(upload + first + b'\033[?1049h' + second + b'\033[?1049l', {})
|
||||||
|
|
||||||
|
# Anonymous upload-and-place does not require a stored source mapping.
|
||||||
|
check(graphics('a=T,f=32,s=1,v=1,p=9,C=1,c=1,r=1', b'\xff' * 4),
|
||||||
|
solid(8, 16, colours[3]))
|
||||||
|
|
||||||
|
# A placement keeps shared pixels after its anonymous source is released.
|
||||||
|
check(graphics('a=T,f=32,s=16,v=32,X=4,Y=8,C=1', raw),
|
||||||
|
pattern(xoff=4, yoff=8))
|
||||||
|
|
||||||
|
# Clients sample the original crop independently, including partial spans.
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
source_width, source_height = 32, 32
|
||||||
|
source = [(255, 0, 0) if (x + y) % 2 else (0, 0, 255)
|
||||||
|
for y in range(source_height) for x in range(source_width)]
|
||||||
|
pixels = bytes(value for colour in source for value in (*colour, 255))
|
||||||
|
# Shrink horizontally and enlarge vertically with transparent offsets.
|
||||||
|
command = graphics('a=T,f=32,s=32,v=32,x=4,y=4,w=24,h=24,'
|
||||||
|
'c=2,r=4,X=4,Y=8,C=1', pixels)
|
||||||
|
test.emit(command)
|
||||||
|
large = test.attach(cw=24, ch=24)
|
||||||
|
|
||||||
|
def expected(cw, ch):
|
||||||
|
"""Map output coordinates through the full placement to its crop."""
|
||||||
|
return {(x, y): source[(4 + (y * 16 - 8 * ch) * 24 // (ch * 56)) * 32 +
|
||||||
|
4 + (x * 8 - 4 * cw) * 24 // (cw * 12)]
|
||||||
|
for y in range(ch // 2, ch * 4) for x in range(cw // 2, cw * 2)}
|
||||||
|
|
||||||
|
def check_clients(clipped=False):
|
||||||
|
"""Check shared-source scaling and alignment on both clients."""
|
||||||
|
test.redraw()
|
||||||
|
terminals = []
|
||||||
|
for attached, cw, ch in [(client, 8, 16), (large, 24, 24)]:
|
||||||
|
term = Terminal(cw=cw, ch=ch)
|
||||||
|
term.feed(attached.data)
|
||||||
|
wanted = expected(cw, ch)
|
||||||
|
if clipped:
|
||||||
|
wanted = {point: colour for point, colour in wanted.items()
|
||||||
|
if point[0] < cw}
|
||||||
|
term.check(wanted)
|
||||||
|
terminals.append((attached, cw, ch, term))
|
||||||
|
return terminals
|
||||||
|
|
||||||
|
terminals = check_clients()
|
||||||
|
test.emit(b'\033[2;1H\033[X')
|
||||||
|
for attached, cw, ch, term in terminals:
|
||||||
|
term.feed(attached.data)
|
||||||
|
term.check(expected(cw, ch))
|
||||||
|
check_clients()
|
||||||
|
# Clip the right-hand span without changing either client's cell pixels.
|
||||||
|
test.run('resize-window', '-x', '1')
|
||||||
|
check_clients(clipped=True)
|
||||||
|
# Changing cell pixels must retain the same partial-span source origin.
|
||||||
|
test.resize(client, 1, 16, cw=24, ch=24)
|
||||||
|
test.redraw()
|
||||||
|
term = Terminal(cw=24, ch=24)
|
||||||
|
term.feed(client.data)
|
||||||
|
wanted = {point: colour for point, colour in expected(24, 24).items()
|
||||||
|
if point[0] < 24}
|
||||||
|
term.check(wanted)
|
||||||
|
|
||||||
|
# Offsets and source crops contribute fractional coverage to text fallback.
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
test.run('set-option', '-as', 'terminal-features', 'screen-256color:RGB')
|
||||||
|
client = test.attach(graphical=False)
|
||||||
|
pixels = b'\xff\0\0\xff' * 16 + b'\0\0\xff\xff' * 16
|
||||||
|
test.emit(graphics('a=T,f=32,s=8,v=4,x=1,w=6,c=1,r=1,X=4,Y=4,C=1',
|
||||||
|
pixels))
|
||||||
|
test.redraw()
|
||||||
|
term = Terminal()
|
||||||
|
term.feed(client.data)
|
||||||
|
char, fg, bg, bold = term.cells[0, 0]
|
||||||
|
assert char == '\u2584' and not bold, (char, bold)
|
||||||
|
assert fg == (31, 0, 95) and bg == (63, 0, 0), (fg, bg)
|
||||||
|
PY
|
||||||
53
regress/image-kitty-prompts.sh
Normal file
53
regress/image-kitty-prompts.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Pane prompts remain above Kitty images during damage and full redraws.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from image_test import image_test, kitty
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
for position, row in [('top', 0), ('bottom', 3)]:
|
||||||
|
with image_test(sys.argv[1], sy=4) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.run('set-option', '-g', 'status-position', position)
|
||||||
|
test.emit(kitty('a=t,i=7,f=32,s=1,v=1', b'\xff\0\0\xff'))
|
||||||
|
test.run('command-prompt', '-P', '-C', '-b', '-l', '-p', 'PROMPT',
|
||||||
|
'display-message %1')
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal(sy=4)
|
||||||
|
terminal.feed(client.data)
|
||||||
|
expected = {(x, y): (255, 0, 0) for y in range(64)
|
||||||
|
for x in range(32) if y // 16 != row}
|
||||||
|
|
||||||
|
client.clear()
|
||||||
|
test.marker += 1
|
||||||
|
title = 'image-%d' % test.marker
|
||||||
|
data = kitty('a=p,i=7,C=1,c=4,r=4') + \
|
||||||
|
b'\033]2;' + title.encode() + b'\007'
|
||||||
|
# Feed the pane directly, bypassing its open prompt.
|
||||||
|
test.run('pipe-pane', '-I', 'printf %%s ' +
|
||||||
|
shlex.quote(data.decode('ascii')))
|
||||||
|
test.wait(lambda: test.run('display-message', '-p',
|
||||||
|
'#{pane_title}') == title)
|
||||||
|
client.read(0.2)
|
||||||
|
test.run('pipe-pane')
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
assert ''.join(terminal.cells[x, row][0] for x in range(6)) \
|
||||||
|
== 'PROMPT', terminal.cells
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal(sy=4)
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
assert ''.join(terminal.cells[x, row][0] for x in range(6)) \
|
||||||
|
== 'PROMPT', terminal.cells
|
||||||
|
PY
|
||||||
432
regress/image-kitty-protocol.sh
Normal file
432
regress/image-kitty-protocol.sh
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Kitty graphics uploads, chunking, queries, placements and replies.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
LC_ALL=C.UTF-8
|
||||||
|
export TERM LC_ALL
|
||||||
|
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
export TEST_TMUX
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - <<'PY'
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shlex
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
tmux = [os.environ['TEST_TMUX'], '-u', '-Limage-protocol' + str(os.getpid()),
|
||||||
|
'-f/dev/null']
|
||||||
|
|
||||||
|
# Run a command on the test server.
|
||||||
|
def run(*args):
|
||||||
|
return subprocess.check_output(tmux + list(args), text=True).strip()
|
||||||
|
|
||||||
|
# Build a Kitty graphics APC sequence.
|
||||||
|
def graphics(control, payload=''):
|
||||||
|
return '\033_G' + control + (';' + payload if payload else '') + '\033\\'
|
||||||
|
|
||||||
|
# Read until a following device-attributes reply, including when q=2 is used.
|
||||||
|
reader = '''
|
||||||
|
import json, os, select, sys, time, tty
|
||||||
|
command, output = sys.argv[1:]
|
||||||
|
tty.setraw(0)
|
||||||
|
with open(command, 'rb') as f:
|
||||||
|
while True:
|
||||||
|
data = f.read(65536)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
while data:
|
||||||
|
data = data[os.write(1, data):]
|
||||||
|
os.write(1, b'\\x1b[c')
|
||||||
|
reply = b''
|
||||||
|
deadline = time.monotonic() + 3
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if select.select([0], [], [], 0.1)[0]:
|
||||||
|
reply += os.read(0, 4096)
|
||||||
|
if b'\\x1b[?' in reply and reply.endswith(b'c'):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise SystemExit('missing device-attributes reply')
|
||||||
|
with open(output, 'w') as f:
|
||||||
|
json.dump(reply.split(b'\\x1b[?')[0].decode(), f)
|
||||||
|
time.sleep(30)
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Check replies, cursor movement and unchanged text and title.
|
||||||
|
def check(name, command, expected, cursor='5,2', timeout=5):
|
||||||
|
output = directory / name
|
||||||
|
command_file = directory / (name + '.input')
|
||||||
|
with command_file.open('w', encoding='utf-8') as f:
|
||||||
|
f.write(origin)
|
||||||
|
if isinstance(command, str):
|
||||||
|
f.write(command)
|
||||||
|
else:
|
||||||
|
f.writelines(command)
|
||||||
|
pane_command = 'python3 -B ' + shlex.quote(str(helper)) + ' ' + \
|
||||||
|
shlex.quote(str(command_file)) + ' ' + shlex.quote(str(output))
|
||||||
|
pane = run('new-window', '-d', '-P', '-F', '#{pane_id}', pane_command)
|
||||||
|
try:
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while not output.exists():
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
raise AssertionError(name + ': missing reply capture')
|
||||||
|
time.sleep(0.05)
|
||||||
|
reply = json.loads(output.read_text())
|
||||||
|
assert reply == expected, (name, 'reply', repr(reply), repr(expected))
|
||||||
|
actual = run('display-message', '-pt', pane, '#{cursor_x},#{cursor_y}')
|
||||||
|
assert actual == cursor, (name, 'cursor', actual, cursor)
|
||||||
|
assert run('capture-pane', '-pt', pane) == '', (name, 'text')
|
||||||
|
title = run('display-message', '-pt', pane, '#{pane_title}')
|
||||||
|
assert title == 'kitty-test', (name, 'title', title)
|
||||||
|
return reply
|
||||||
|
finally:
|
||||||
|
run('kill-window', '-t', pane)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
|
||||||
|
directory = Path(tmp)
|
||||||
|
helper = directory / 'read-reply.py'
|
||||||
|
helper.write_text(reader)
|
||||||
|
origin = '\033]2;kitty-test\007\033[3;6H'
|
||||||
|
try:
|
||||||
|
run('new-session', '-d', '-x', '40', '-y', '12')
|
||||||
|
if run('display-message', '-p', '#{image_support}') == '0':
|
||||||
|
raise SystemExit(101)
|
||||||
|
run('set', '-g', 'status', 'off')
|
||||||
|
pixel = '/wAA/w=='
|
||||||
|
transmit = 'a=t,i=7,f=32,s=1,v=1'
|
||||||
|
ok = graphics('i=7', 'OK')
|
||||||
|
invalid = graphics('i=7', 'EINVAL')
|
||||||
|
check('transmit', graphics(transmit, pixel), ok)
|
||||||
|
check('defaults', graphics('i=7,s=1,v=1', pixel), ok)
|
||||||
|
check('rgb', graphics('a=t,i=7,f=24,s=1,v=1', '/wAA'), ok)
|
||||||
|
check('unpadded', graphics(transmit, '/wAA/w'), ok)
|
||||||
|
check('placement-id-reply', graphics(transmit + ',p=9', pixel),
|
||||||
|
graphics('i=7,p=9', 'OK'))
|
||||||
|
check('chunks', graphics(transmit + ',m=1', '/wAA') +
|
||||||
|
graphics('m=0', '/w=='), ok)
|
||||||
|
check('nonzero-padding', graphics(transmit + ',m=1', '/wAA') +
|
||||||
|
graphics('m=0', '/z=='), invalid)
|
||||||
|
check('empty-decoded-chunk', graphics(transmit + ',m=1', ' ') +
|
||||||
|
graphics('m=0', pixel), ok)
|
||||||
|
check('empty-final-chunk', graphics(transmit + ',m=1', pixel) +
|
||||||
|
graphics('m=0'), ok)
|
||||||
|
check('incomplete', graphics(transmit + ',m=1', '/wAA'), '')
|
||||||
|
check('failed-chunk', graphics(transmit + ',m=1', '/wAA') +
|
||||||
|
graphics('m=0', '!!!!') + graphics(transmit, pixel), invalid + ok)
|
||||||
|
check('quiet-ok', graphics(transmit + ',q=1', pixel), '')
|
||||||
|
check('quiet-two', graphics(transmit + ',q=2', pixel), '')
|
||||||
|
check('quiet-one-error', graphics(transmit + ',q=1', '!!!!'), invalid)
|
||||||
|
check('quiet-two-error', graphics(transmit + ',q=2', '!!!!'), '')
|
||||||
|
check('quiet-continuation', graphics(transmit + ',q=2,m=1', '/wAA') +
|
||||||
|
graphics('m=0', '/w=='), '')
|
||||||
|
check('quiet-control-error', graphics('a=t,q=2,i=7,s=bad'), '')
|
||||||
|
check('anonymous', graphics('a=t,f=32,s=1,v=1', pixel), '')
|
||||||
|
check('anonymous-error', graphics('a=t,f=32,s=1,v=1', '!!!!'), '')
|
||||||
|
check('query', graphics('a=q,f=32,s=1,v=1', pixel),
|
||||||
|
graphics('i=0', 'OK'))
|
||||||
|
check('query-chunks', graphics('a=q,i=7,f=32,s=1,v=1,m=1', '/wAA') +
|
||||||
|
graphics('m=0', '/w=='), ok)
|
||||||
|
check('replace', graphics(transmit, pixel) +
|
||||||
|
graphics(transmit, 'AP8A/w=='), ok + ok)
|
||||||
|
check('transmit-and-place',
|
||||||
|
graphics('a=T,i=7,f=32,s=1,v=1', pixel), ok, cursor='6,2')
|
||||||
|
check('transmit-and-place-sized',
|
||||||
|
graphics('a=T,i=7,f=32,s=1,v=1,c=3,r=2', pixel),
|
||||||
|
ok, cursor='8,3')
|
||||||
|
check('place-stored', graphics(transmit + ',q=2', pixel) +
|
||||||
|
graphics('a=p,i=7'), ok, cursor='6,2')
|
||||||
|
check('place-no-cursor', graphics(transmit + ',q=2', pixel) +
|
||||||
|
graphics('a=p,i=7,C=1,c=3,r=2'), ok)
|
||||||
|
check('place-named', graphics(transmit + ',q=2', pixel) +
|
||||||
|
graphics('a=p,i=7,p=9,C=1'), graphics('i=7,p=9', 'OK'))
|
||||||
|
check('place-missing', graphics('a=p,i=7'),
|
||||||
|
graphics('i=7', 'ENOENT'))
|
||||||
|
check('query-does-not-store',
|
||||||
|
graphics('a=q,q=2,i=7,f=32,s=1,v=1', pixel) +
|
||||||
|
graphics('a=p,i=7'), graphics('i=7', 'ENOENT'))
|
||||||
|
check('place-chunks', graphics('a=T,i=7,f=32,s=1,v=1,'
|
||||||
|
'c=3,r=2,C=1,m=1', '/wAA') + graphics('m=0', '/w=='), ok)
|
||||||
|
for n, geometry in enumerate(['x=1', 'y=1', 'c=65536', 'r=65536',
|
||||||
|
'c=65534,r=32767']):
|
||||||
|
check('invalid-placement-' + str(n),
|
||||||
|
graphics(transmit + ',q=2', pixel) +
|
||||||
|
graphics('a=p,i=7,C=1,' + geometry), invalid)
|
||||||
|
check('failed-transmit-keeps-source',
|
||||||
|
graphics(transmit + ',q=2', pixel) +
|
||||||
|
graphics('a=T,i=7,f=32,s=1,v=1,x=1,C=1', pixel) +
|
||||||
|
graphics('a=p,i=7,C=1'), invalid + ok)
|
||||||
|
check('place-at-bottom', '\033[12;6H' +
|
||||||
|
graphics('a=T,i=7,f=32,s=1,v=1,c=3,r=2', pixel),
|
||||||
|
ok, cursor='8,11')
|
||||||
|
check('stationary-at-bottom', '\033[12;6H' +
|
||||||
|
graphics('a=T,i=7,f=32,s=1,v=1,c=3,r=2,C=1', pixel),
|
||||||
|
ok, cursor='5,11')
|
||||||
|
for n, payload in enumerate(['', ' ', '!!!!', '====', '/w',
|
||||||
|
'/wAA/wAA']):
|
||||||
|
check('invalid-payload-' + str(n),
|
||||||
|
graphics(transmit, payload), invalid)
|
||||||
|
for control in ['f=12,s=1,v=1', 'f=32,s=0,v=1', 'f=32,s=1,v=0',
|
||||||
|
'f=32,s=bad,v=1', 'f=32,s=4294967296,v=1',
|
||||||
|
'f=32,s=1,v=1,o=x']:
|
||||||
|
check('invalid-control-' + control,
|
||||||
|
graphics('a=t,i=7,' + control, pixel), invalid)
|
||||||
|
for action in ['f', 'a', 'c']:
|
||||||
|
check('unsupported-action-' + action,
|
||||||
|
graphics('a=%s,i=7' % action), graphics('i=7', 'ENOTSUP'))
|
||||||
|
for medium in ['f', 't', 's']:
|
||||||
|
check('unsupported-medium-' + medium,
|
||||||
|
graphics('a=t,i=7,t=' + medium), graphics('i=7', 'ENOTSUP'))
|
||||||
|
check('unsupported-number', graphics('a=t,I=7,f=32,s=1,v=1', pixel),
|
||||||
|
graphics('I=7', 'ENOTSUP'))
|
||||||
|
check('id-and-number', graphics('a=t,i=7,I=8,f=32,s=1,v=1', pixel),
|
||||||
|
graphics('i=7,I=8', 'EINVAL'))
|
||||||
|
check('unknown-extension', graphics(transmit + ',k=9', pixel), ok)
|
||||||
|
check('unknown-extension-string',
|
||||||
|
graphics(transmit + ',k=unused', pixel), ok)
|
||||||
|
check('trailing-comma', graphics(transmit + ',', pixel), ok)
|
||||||
|
check('maximum-ids',
|
||||||
|
graphics('a=t,i=4294967295,p=4294967295,f=32,s=1,v=1', pixel),
|
||||||
|
graphics('i=4294967295,p=4294967295', 'OK'))
|
||||||
|
unsupported = graphics('i=7', 'ENOTSUP')
|
||||||
|
check('default-layer', graphics(transmit + ',z=0', pixel), ok)
|
||||||
|
for z in [-2147483648, -1073741824, -1, 1, 2147483647]:
|
||||||
|
check('unsupported-layer-' + str(z),
|
||||||
|
graphics(transmit + ',z=' + str(z), pixel), unsupported)
|
||||||
|
for action in ['t', 'T', 'p', 'q', 'd']:
|
||||||
|
check('unsupported-action-layer-' + action,
|
||||||
|
graphics('a=%s,i=7,p=9,f=32,s=1,v=1,z=-1' % action,
|
||||||
|
pixel), graphics('i=7,p=9', 'ENOTSUP'))
|
||||||
|
for quiet in [1, 2]:
|
||||||
|
check('unsupported-layer-quiet-' + str(quiet),
|
||||||
|
graphics(transmit + ',q=%d,z=1' % quiet, pixel),
|
||||||
|
unsupported if quiet == 1 else '')
|
||||||
|
check('zero-layer-chunks', graphics(transmit + ',z=0,m=1', '/wAA') +
|
||||||
|
graphics('m=0,z=0', '/w=='), ok)
|
||||||
|
check('unsupported-layer-aborts-chunks',
|
||||||
|
graphics(transmit + ',m=1', '/wAA') +
|
||||||
|
graphics('m=0,z=1', '/w==') + graphics(transmit, pixel),
|
||||||
|
unsupported + ok)
|
||||||
|
check('unsupported-layer-does-not-start-chunks',
|
||||||
|
graphics(transmit + ',m=1,z=1', '/wAA') +
|
||||||
|
graphics('m=0', '/w==') + graphics(transmit, pixel),
|
||||||
|
unsupported + ok)
|
||||||
|
# Reply controls must be read even after an unsupported field.
|
||||||
|
check('quiet-after-layer',
|
||||||
|
graphics(transmit + ',z=1,q=2', pixel), '')
|
||||||
|
check('id-after-layer',
|
||||||
|
graphics('a=q,z=1,i=7,f=32,s=1,v=1', pixel), unsupported)
|
||||||
|
check('placement-after-layer',
|
||||||
|
graphics('z=-1,a=q,i=7,p=9,q=1', pixel),
|
||||||
|
graphics('i=7,p=9', 'ENOTSUP'))
|
||||||
|
check('quiet-after-invalid-control',
|
||||||
|
graphics('a=t,i=7,s=bad,q=2', pixel), '')
|
||||||
|
check('id-after-invalid-control',
|
||||||
|
graphics('s=bad,a=t,i=7,p=9', pixel),
|
||||||
|
graphics('i=7,p=9', 'EINVAL'))
|
||||||
|
check('id-after-malformed-control',
|
||||||
|
graphics('broken,a=q,i=7,p=9', pixel),
|
||||||
|
graphics('i=7,p=9', 'EINVAL'))
|
||||||
|
check('quiet-after-malformed-control',
|
||||||
|
graphics('broken,a=t,i=7,q=2', pixel), '')
|
||||||
|
check('first-error-invalid',
|
||||||
|
graphics('s=bad,z=1,a=q,i=7,p=9', pixel),
|
||||||
|
graphics('i=7,p=9', 'EINVAL'))
|
||||||
|
check('first-error-unsupported',
|
||||||
|
graphics('z=1,s=bad,a=q,i=7,p=9', pixel),
|
||||||
|
graphics('i=7,p=9', 'ENOTSUP'))
|
||||||
|
for control in ['U=1', 'P=99', 'Q=9', 'H=1', 'H=-1', 'V=1', 'V=-1']:
|
||||||
|
check('unsupported-placement-' + control,
|
||||||
|
graphics('a=T,i=7,f=32,s=1,v=1,' + control, pixel),
|
||||||
|
unsupported)
|
||||||
|
check('quiet-unsupported-placement-' + control,
|
||||||
|
graphics('a=T,i=7,' + control + ',q=2', pixel), '')
|
||||||
|
check('zero-placement-extensions', graphics(transmit +
|
||||||
|
',U=0,P=0,Q=0,H=0,V=0', pixel), ok)
|
||||||
|
for z in ['-2147483649', '2147483648', '1x', '', 'bad']:
|
||||||
|
check('invalid-layer-' + z,
|
||||||
|
graphics(transmit + ',z=' + z, pixel), invalid)
|
||||||
|
for n, control in enumerate(['s=', 's=-1', 's=1x', 'a=tt', 't=dd',
|
||||||
|
'o=zz', '=1', 'ss=1', ',s=1',
|
||||||
|
's=1,,v=1']):
|
||||||
|
check('malformed-control-' + str(n),
|
||||||
|
graphics('i=7,' + control, pixel), invalid)
|
||||||
|
check('failed-control-resets-transfer',
|
||||||
|
graphics(transmit + ',m=1', '/wAA') +
|
||||||
|
graphics('m=0,s=bad') + graphics(transmit, pixel), invalid + ok)
|
||||||
|
check('quiet-resets', graphics(transmit + ',q=2', pixel) +
|
||||||
|
graphics(transmit, pixel), ok)
|
||||||
|
check('oversized-dimensions',
|
||||||
|
graphics('a=t,i=7,f=32,s=4294967295,v=4294967295', pixel),
|
||||||
|
invalid)
|
||||||
|
check('delete-aborts-upload', graphics(transmit + ',q=2,m=1', '/wAA') +
|
||||||
|
graphics('a=d,d=I,q=2') + graphics(transmit, pixel), ok)
|
||||||
|
|
||||||
|
# Successful deletion sends no reply and never moves the cursor.
|
||||||
|
stored = graphics(transmit + ',q=2', pixel)
|
||||||
|
put = 'a=p,q=2,i=7,C=1'
|
||||||
|
first = graphics(put + ',p=1')
|
||||||
|
second = graphics(put + ',p=2')
|
||||||
|
missing = graphics('i=7', 'ENOENT')
|
||||||
|
check('delete-soft-image', stored + first + graphics('a=d,d=i,i=7') +
|
||||||
|
graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('delete-hard-image', stored + first + graphics('a=d,d=I,i=7') +
|
||||||
|
graphics('a=p,i=7,C=1'), missing)
|
||||||
|
check('delete-one-placement', stored + first + second +
|
||||||
|
graphics('a=d,d=I,i=7,p=1') + graphics('a=p,i=7,p=2,C=1'),
|
||||||
|
graphics('i=7,p=2', 'OK'))
|
||||||
|
check('delete-last-placement', stored + first +
|
||||||
|
graphics('a=d,d=I,i=7,p=1') + graphics('a=p,i=7,C=1'), missing)
|
||||||
|
transparent = graphics('a=T,i=7,p=1,q=2,C=1,f=32,s=1,v=1',
|
||||||
|
'/wAAAA==')
|
||||||
|
for selector in ['a', 'i', 'A', 'I']:
|
||||||
|
check('delete-transparent-' + selector, transparent +
|
||||||
|
graphics('a=d,d=%s,i=7,p=1' % selector) +
|
||||||
|
graphics('a=p,i=7,C=1'),
|
||||||
|
missing if selector.isupper() else ok)
|
||||||
|
check('delete-transparent-retains-other-placement', transparent +
|
||||||
|
second + graphics('a=d,d=I,i=7,p=1') +
|
||||||
|
graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('delete-retain-unnumbered', stored + first + graphics(put) +
|
||||||
|
graphics('a=d,d=I,i=7,p=1') + graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('delete-missing-placement', stored + first +
|
||||||
|
graphics('a=d,d=I,i=7,p=99') + graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('delete-upload-only', stored + graphics('a=d,d=I,i=7') +
|
||||||
|
graphics('a=p,i=7,C=1'), missing)
|
||||||
|
check('delete-soft-all', stored + first + graphics('a=d,d=a') +
|
||||||
|
graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('delete-hard-all', stored + first + graphics('a=d,d=A') +
|
||||||
|
graphics('a=p,i=7,C=1'), missing)
|
||||||
|
check('delete-all-retains-unplaced', stored + graphics('a=d,d=A') +
|
||||||
|
graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('delete-history-retains-data', stored + first + '\033[12S' +
|
||||||
|
graphics('a=d,d=A') + graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('delete-history-by-id', stored + first + '\033[12S' +
|
||||||
|
graphics('a=d,d=I,i=7') + graphics('a=p,i=7,C=1'), missing)
|
||||||
|
for selector in ['A', 'I']:
|
||||||
|
check('delete-saved-screen-' + selector, stored + first +
|
||||||
|
'\033[?1049h' + graphics('a=d,d=' + selector + ',i=7') +
|
||||||
|
'\033[?1049l' + graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('delete-abort-does-not-inherit-id', stored +
|
||||||
|
graphics(transmit + ',q=2,m=1', '/wAA') +
|
||||||
|
graphics('a=d,d=I') + graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('unsupported-delete', stored + graphics('a=d,d=?,i=7'),
|
||||||
|
graphics('i=7', 'ENOTSUP'))
|
||||||
|
check('malformed-delete', stored + graphics('a=d,i=7,d=ii'), invalid)
|
||||||
|
|
||||||
|
# Unsupported selectors leave uploaded data and placements intact.
|
||||||
|
for selector in 'cCpPqQrRxXyYzZ':
|
||||||
|
control = 'a=d,d=%s,i=7,x=6,y=3,z=0' % selector
|
||||||
|
check('unsupported-delete-' + selector, stored + first +
|
||||||
|
graphics(control) + graphics('a=p,i=7,C=1'),
|
||||||
|
unsupported + ok)
|
||||||
|
for quiet in [1, 2]:
|
||||||
|
check('unsupported-delete-quiet-' + str(quiet), stored + first +
|
||||||
|
graphics('a=d,d=P,i=7,q=%d,x=6,y=3' % quiet) +
|
||||||
|
graphics('a=p,i=7,C=1'),
|
||||||
|
(unsupported if quiet == 1 else '') + ok)
|
||||||
|
check('unsupported-delete-named', stored + first +
|
||||||
|
graphics('a=d,d=Z,i=7,p=1') + graphics('a=p,i=7,C=1'),
|
||||||
|
graphics('i=7,p=1', 'ENOTSUP') + ok)
|
||||||
|
check('unsupported-delete-aborts-upload', stored +
|
||||||
|
graphics(transmit + ',q=2,m=1', '/wAA') +
|
||||||
|
graphics('a=d,d=P,i=7') + graphics('a=p,i=7,C=1'),
|
||||||
|
unsupported + ok)
|
||||||
|
check('unsupported-delete-does-not-inherit-id', stored +
|
||||||
|
graphics(transmit + ',m=1', '/wAA') + graphics('a=d,d=P') +
|
||||||
|
graphics('a=p,i=7,C=1'), ok)
|
||||||
|
check('unsupported-layer-delete-aborts-upload', stored +
|
||||||
|
graphics(transmit + ',q=2,m=1', '/wAA') +
|
||||||
|
graphics('a=d,d=I,i=7,z=1') + graphics('a=p,i=7,C=1'),
|
||||||
|
unsupported + ok)
|
||||||
|
check('unsupported-layer-delete-does-not-inherit-id', stored +
|
||||||
|
graphics(transmit + ',m=1', '/wAA') +
|
||||||
|
graphics('a=d,d=I,z=-1') + graphics('a=p,i=7,C=1'), ok)
|
||||||
|
for action in ['t', 'T', 'q']:
|
||||||
|
check('unsupported-layer-keeps-source-' + action, stored +
|
||||||
|
graphics('a=%s,i=7,f=32,s=1,v=1,z=1' % action, pixel) +
|
||||||
|
graphics('a=p,i=7,C=1'), unsupported + ok)
|
||||||
|
check('unsupported-layer-query-does-not-store',
|
||||||
|
graphics('a=q,i=7,f=32,s=1,v=1,z=1', pixel) +
|
||||||
|
graphics('a=p,i=7,C=1'), unsupported + missing)
|
||||||
|
|
||||||
|
# Abort immediately when chunked data exceeds 64 MiB, then recover.
|
||||||
|
block = base64.b64encode(b'\0' * (512 * 1024)).decode()
|
||||||
|
commands = [graphics(transmit + ',q=1,m=1', block)]
|
||||||
|
commands.extend([graphics('m=1', block)] * 127)
|
||||||
|
commands.extend([graphics('m=1', 'AAAAAA=='),
|
||||||
|
graphics('a=t,i=8,f=32,s=1,v=1', pixel)])
|
||||||
|
check('accumulated-size-limit', commands,
|
||||||
|
invalid + graphics('i=8', 'OK'), timeout=30)
|
||||||
|
|
||||||
|
# Compressed uploads require a test build with zlib.
|
||||||
|
compressed = base64.b64encode(zlib.compress(b'\xff\0\0\xff')).decode()
|
||||||
|
control = transmit + ',o=z'
|
||||||
|
for format, raw in [(24, b'\xff\0\0'), (32, b'\xff\0\0\xff')]:
|
||||||
|
payload = base64.b64encode(zlib.compress(raw)).decode()
|
||||||
|
check('compressed-' + str(format),
|
||||||
|
graphics('a=t,i=7,f=%d,s=1,v=1,o=z' % format, payload),
|
||||||
|
ok)
|
||||||
|
split = len(compressed) // 8 * 4
|
||||||
|
check('compressed-chunks', graphics(control + ',m=1',
|
||||||
|
compressed[:split]) + graphics('m=0', compressed[split:]), ok)
|
||||||
|
check('compressed-truncated', graphics(control, compressed[:-4]),
|
||||||
|
invalid)
|
||||||
|
check('compressed-wrong-size',
|
||||||
|
graphics(control.replace('s=1', 's=2'), compressed), invalid)
|
||||||
|
large = base64.b64encode(zlib.compress(
|
||||||
|
b'\0' * (4096 * 4097 * 3))).decode()
|
||||||
|
check('oversized-rgb',
|
||||||
|
graphics('a=t,i=7,f=24,s=4096,v=4097,o=z', large), invalid)
|
||||||
|
|
||||||
|
# PNG uploads require a test build with libpng.
|
||||||
|
png = b'\x89PNG\r\n\x1a\n'
|
||||||
|
for kind, data in [
|
||||||
|
(b'IHDR', struct.pack('>2I5B', 8, 16, 8, 6, 0, 0, 0)),
|
||||||
|
(b'IDAT', zlib.compress((b'\0' + b'\xff\0\0\xff' * 8) * 16)),
|
||||||
|
(b'IEND', b'')]:
|
||||||
|
png += struct.pack('>I', len(data)) + kind + data + \
|
||||||
|
struct.pack('>I', zlib.crc32(kind + data))
|
||||||
|
payload = base64.b64encode(png).decode()
|
||||||
|
control = 'a=t,i=7,f=100'
|
||||||
|
check('png', graphics(control, payload), ok)
|
||||||
|
truncated = base64.b64encode(png[:40]).decode()
|
||||||
|
check('png-truncated', graphics(control, truncated), invalid)
|
||||||
|
damaged = bytearray(png)
|
||||||
|
damaged[29] ^= 1
|
||||||
|
check('png-bad-header-crc',
|
||||||
|
graphics(control, base64.b64encode(damaged).decode()), invalid)
|
||||||
|
split = len(payload) // 8 * 4
|
||||||
|
check('png-chunks', graphics(control + ',m=1', payload[:split]) +
|
||||||
|
graphics('m=0', payload[split:]), ok)
|
||||||
|
payload = base64.b64encode(zlib.compress(png)).decode()
|
||||||
|
control += ',o=z'
|
||||||
|
sized = control + ',S=' + str(len(png))
|
||||||
|
check('compressed-png', graphics(sized, payload), ok)
|
||||||
|
split = len(payload) // 8 * 4
|
||||||
|
check('compressed-png-chunks',
|
||||||
|
graphics(sized + ',m=1', payload[:split]) +
|
||||||
|
graphics('m=0', payload[split:]), ok)
|
||||||
|
check('compressed-png-missing-size', graphics(control, payload),
|
||||||
|
invalid)
|
||||||
|
check('compressed-png-wrong-size',
|
||||||
|
graphics(control + ',S=' + str(len(png) + 1), payload),
|
||||||
|
invalid)
|
||||||
|
check('compressed-png-oversized',
|
||||||
|
graphics(control + ',S=67108865', payload), invalid)
|
||||||
|
check('compressed-png-truncated', graphics(sized, payload[:-4]),
|
||||||
|
invalid)
|
||||||
|
check('decoder-controls-reset', graphics(sized + ',q=2', payload) +
|
||||||
|
graphics('a=T,i=7,s=1,v=1', pixel), ok, cursor='6,2')
|
||||||
|
finally:
|
||||||
|
subprocess.run(tmux + ['kill-server'], stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL)
|
||||||
|
PY
|
||||||
102
regress/image-kitty-transfers.sh
Executable file
102
regress/image-kitty-transfers.sh
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Kitty transfer state, fragmented APCs, interleaved text and failed uploads.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import sys
|
||||||
|
from image_test import image_test, kitty
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
def check(test, client, expected):
|
||||||
|
"""Check immediate output and independently redrawn image pixels."""
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
|
||||||
|
|
||||||
|
red, blue = (255, 0, 0), (0, 0, 255)
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
test.run('set-option', '-g', 'scroll-on-clear', 'off')
|
||||||
|
client = test.attach()
|
||||||
|
|
||||||
|
# Many separately encoded chunks reconstruct exactly the original pixels.
|
||||||
|
w, h = 64, 32
|
||||||
|
colours = [red if (x // 8 + y // 8) % 2 else blue
|
||||||
|
for y in range(h) for x in range(w)]
|
||||||
|
raw = bytes(c for pixel in colours for c in (*pixel, 255))
|
||||||
|
command = kitty('a=T,i=7,p=9,C=1,f=32,s=64,v=32,m=1', raw[:128])
|
||||||
|
for offset in range(128, len(raw), 128):
|
||||||
|
more = int(offset + 128 < len(raw))
|
||||||
|
command += kitty('m=%d' % more, raw[offset:offset + 128])
|
||||||
|
test.emit(command)
|
||||||
|
check(test, client, {(x, y): colours[y * w + x]
|
||||||
|
for y in range(h) for x in range(w)})
|
||||||
|
|
||||||
|
# A query must neither replace the existing source nor draw its own pixels.
|
||||||
|
test.emit(kitty('a=q,i=7,f=32,s=1,v=1', b'\0\xff\0\xff') +
|
||||||
|
b'\033[2J\033[H' + kitty('a=p,i=7,p=9,C=1'))
|
||||||
|
expected = {(x, y): colours[y * w + x]
|
||||||
|
for y in range(h) for x in range(w)}
|
||||||
|
check(test, client, expected)
|
||||||
|
|
||||||
|
# Failed decoding must leave the earlier source and its placement intact.
|
||||||
|
for control, data in [('f=24,s=1,v=1', b'\xff\0'),
|
||||||
|
('f=32,s=1,v=1,o=z', b'not zlib'),
|
||||||
|
('f=100', b'not PNG')]:
|
||||||
|
test.emit(kitty('a=T,i=7,p=9,C=1,' + control, data))
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
test.emit(b'\033[2J\033[H' + kitty('a=p,i=7,p=9,C=1'))
|
||||||
|
check(test, client, expected)
|
||||||
|
|
||||||
|
# Text and cursor movement between chunks do not reset the upload.
|
||||||
|
test.emit(b'\033[2J\033[H' + kitty(
|
||||||
|
'a=T,i=7,p=9,f=32,s=1,v=1,c=2,r=2,m=1', b'\xff\0\0'))
|
||||||
|
assert not client.images(), 'incomplete upload drew an image'
|
||||||
|
assert test.run('display-message', '-p', '#{cursor_x},#{cursor_y}') == '0,0'
|
||||||
|
test.emit(b'between\033[3;5H')
|
||||||
|
test.emit(kitty('m=0', b'\xff'))
|
||||||
|
check(test, client, {(x, y): red
|
||||||
|
for y in range(32, 64) for x in range(32, 48)})
|
||||||
|
assert test.run('display-message', '-p', '#{cursor_x},#{cursor_y}') == '6,3'
|
||||||
|
assert test.run('capture-pane', '-p') == 'between'
|
||||||
|
|
||||||
|
# Split the introducer, controls, payload and terminator across writes.
|
||||||
|
test.emit(b'\033[2J\033[H')
|
||||||
|
wire = kitty('a=T,i=7,p=9,C=1,f=32,s=1,v=1,c=1,r=1', b'\0\0\xff\xff')
|
||||||
|
cuts = [1, 2, 3, wire.index(b';'), wire.index(b';') + 3, len(wire) - 1]
|
||||||
|
offset = 0
|
||||||
|
for end in cuts:
|
||||||
|
test.send(wire[offset:end])
|
||||||
|
test.wait(lambda: test.run('capture-pane', '-p', '-P') ==
|
||||||
|
wire[:end].decode())
|
||||||
|
if end < len(wire) - 1:
|
||||||
|
assert not client.images(), 'unfinished APC drew an image'
|
||||||
|
offset = end
|
||||||
|
# The input parser can dispatch the APC when it receives the ST's ESC.
|
||||||
|
client.read(0.2)
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
test.emit(wire[offset:])
|
||||||
|
terminal.feed(client.data)
|
||||||
|
expected = {(x, y): blue for y in range(16) for x in range(8)}
|
||||||
|
terminal.check(expected)
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
assert test.run('capture-pane', '-p') == '', 'APC bytes appeared as text'
|
||||||
|
PY
|
||||||
141
regress/image-limits.sh
Normal file
141
regress/image-limits.sh
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Image quotas evict uploads and placements across protocols and saved grids.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import sys
|
||||||
|
from image_test import image_test, kitty, sixel
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
def upload(image_id, colour):
|
||||||
|
"""Upload one solid source pixel without placing it."""
|
||||||
|
return kitty('a=t,f=32,s=1,v=1,i=%d' % image_id, bytes((*colour, 255)))
|
||||||
|
|
||||||
|
|
||||||
|
def place(image_id, placement_id=0):
|
||||||
|
"""Place one image at the cursor without moving the cursor."""
|
||||||
|
return kitty('a=p,i=%d,p=%d,c=1,r=1,C=1' % (image_id, placement_id))
|
||||||
|
|
||||||
|
|
||||||
|
def cell(colour, x=0, y=0):
|
||||||
|
"""Expand a solid image cell into positioned pixels."""
|
||||||
|
return {(px, py): colour for py in range(y * 16, (y + 1) * 16)
|
||||||
|
for px in range(x * 8, (x + 1) * 8)}
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(test, client, expected):
|
||||||
|
"""Check retained images after a full redraw."""
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal()
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
|
||||||
|
|
||||||
|
red, green, blue = (255, 0, 0), (0, 255, 0), (0, 0, 255)
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(upload(1, red) +
|
||||||
|
b''.join(upload(i, green) for i in range(2, 1001)))
|
||||||
|
test.emit(upload(1001, blue))
|
||||||
|
# The oldest upload is evicted and its application ID no longer resolves.
|
||||||
|
test.emit(place(1))
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.emit(place(1001))
|
||||||
|
snapshot(test, client, cell(blue))
|
||||||
|
test.emit(b'\033[3;1H' + place(2))
|
||||||
|
snapshot(test, client, cell(blue))
|
||||||
|
test.emit(place(1000))
|
||||||
|
snapshot(test, client, cell(blue) | cell(green, y=2))
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(upload(1, red) + place(1, 1))
|
||||||
|
test.emit(upload(2, green))
|
||||||
|
test.emit(kitty('a=d,d=i,i=1,p=1'))
|
||||||
|
# Losing the last placement keeps the source's original last use order.
|
||||||
|
test.emit(b''.join(upload(i, blue) for i in range(3, 1002)))
|
||||||
|
test.emit(place(2))
|
||||||
|
snapshot(test, client, cell(green))
|
||||||
|
test.emit(b'\033[3;1H' + place(1))
|
||||||
|
snapshot(test, client, cell(green))
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(upload(1, red) + b'\033[7;1H' + place(1, 1) +
|
||||||
|
b'\033[H' + place(1, 2) + b'\033[3;1H' + place(1, 3) +
|
||||||
|
b'\033[5;1H' + b''.join(place(1, p) for p in range(4, 1000)))
|
||||||
|
snapshot(test, client,
|
||||||
|
cell(red) | cell(red, y=2) | cell(red, y=4) | cell(red, y=6))
|
||||||
|
# Redraw order differs from creation order: the top view is oldest.
|
||||||
|
test.emit(upload(2, green))
|
||||||
|
snapshot(test, client, cell(red, y=2) | cell(red, y=4) | cell(red, y=6))
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(upload(1, red) + b''.join(place(1, p) for p in range(1, 1000)))
|
||||||
|
test.emit(upload(2, green) + place(2, 1))
|
||||||
|
snapshot(test, client, cell(green))
|
||||||
|
# The shared source survives; only its two oldest views were evicted.
|
||||||
|
test.emit(kitty('a=d,d=I,i=2'))
|
||||||
|
snapshot(test, client, cell(red))
|
||||||
|
test.emit(b''.join(kitty('a=d,d=i,i=1,p=%d' % p)
|
||||||
|
for p in range(3, 1000)))
|
||||||
|
snapshot(test, client, {})
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(sixel(8, 16, [(100, 0, 0)], [0] * (8 * 16)))
|
||||||
|
# Kitty views share the same limit as the older SIXEL source.
|
||||||
|
test.emit(b'\033[3;1H' + upload(1, green) +
|
||||||
|
b''.join(place(1, p) for p in range(1, 1000)))
|
||||||
|
snapshot(test, client, cell(green, y=2))
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(upload(1, red) + place(1))
|
||||||
|
test.emit(b'\033[3;1H' + upload(2, green) +
|
||||||
|
b''.join(place(2, p) for p in range(1, 998)))
|
||||||
|
snapshot(test, client, cell(red) | cell(green, y=2))
|
||||||
|
# SIXEL input also evicts Kitty placements and their uploaded image IDs.
|
||||||
|
test.emit(b'\033[5;1H' +
|
||||||
|
sixel(8, 16, [(0, 0, 100)], [0] * (8 * 16)))
|
||||||
|
snapshot(test, client, cell(green, y=2) | cell(blue, y=4))
|
||||||
|
test.emit(b'\033[7;1H' +
|
||||||
|
sixel(8, 16, [(0, 0, 100)], [0] * (8 * 16)))
|
||||||
|
snapshot(test, client, cell(green, y=2) | cell(blue, y=4) | cell(blue, y=6))
|
||||||
|
test.emit(b'\033[H' + place(1))
|
||||||
|
snapshot(test, client, cell(green, y=2) | cell(blue, y=4) | cell(blue, y=6))
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(upload(1, red) + place(1))
|
||||||
|
test.emit(b'\033[?1049h\033[H' + upload(2, green) +
|
||||||
|
b''.join(place(2, p) for p in range(1, 1000)))
|
||||||
|
# Eviction reaches the saved primary grid and removes its Kitty ID.
|
||||||
|
test.emit(b'\033[?1049l')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.emit(place(1))
|
||||||
|
snapshot(test, client, {})
|
||||||
|
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
test.emit(sixel(8, 16, [(100, 0, 0)], [0] * (8 * 16)))
|
||||||
|
test.run('copy-mode')
|
||||||
|
# Keep the copied grid alive in a hidden window while exceeding the quota.
|
||||||
|
test.run('new-window', '-n', 'quota', '/bin/sh', '-c',
|
||||||
|
"stty raw -echo; printf '\033]2;QUOTA\007'; exec cat")
|
||||||
|
test.wait(lambda: test.run('display-message', '-p',
|
||||||
|
'#{pane_title}') == 'QUOTA')
|
||||||
|
test.emit(upload(1, green) + b''.join(place(1, p) for p in range(1, 1000)))
|
||||||
|
test.run('select-window', '-t', 'image:0')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
test.run('send-keys', '-X', 'cancel')
|
||||||
|
snapshot(test, client, {})
|
||||||
|
PY
|
||||||
55
regress/image-resize-height.sh
Normal file
55
regress/image-resize-height.sh
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Height reduction discards image rows without leaving spans behind.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
export TERM
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
command -v python3 >/dev/null || exit 101
|
||||||
|
|
||||||
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
|
import sys
|
||||||
|
from image_test import image_test, kitty, sixel
|
||||||
|
from image_terminal import Terminal
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(test, client, expected, sy):
|
||||||
|
"""Check image pixels after a full redraw at the current height."""
|
||||||
|
test.redraw()
|
||||||
|
terminal = Terminal(sy=sy)
|
||||||
|
terminal.feed(client.data)
|
||||||
|
terminal.check(expected)
|
||||||
|
|
||||||
|
|
||||||
|
red, green = (255, 0, 0), (0, 255, 0)
|
||||||
|
for protocol in ('kitty', 'sixel'):
|
||||||
|
if protocol == 'kitty':
|
||||||
|
image = kitty('a=T,f=32,s=1,v=1,c=2,r=5,C=1', bytes((*red, 255)))
|
||||||
|
lower = kitty('a=T,f=32,s=1,v=1,c=2,r=1,C=1', bytes((*green, 255)))
|
||||||
|
else:
|
||||||
|
image = sixel(16, 80, [(100, 0, 0)], [0] * (16 * 80))
|
||||||
|
lower = sixel(16, 16, [(0, 100, 0)], [0] * (16 * 16))
|
||||||
|
for alternate in (False, True):
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
|
client = test.attach()
|
||||||
|
if alternate:
|
||||||
|
test.emit(b'\033[?1049h')
|
||||||
|
test.emit(b'\033[9;3H' + image + b'\033[14;3H' + lower +
|
||||||
|
b'\033[H')
|
||||||
|
expected = {(x, y): red for y in range(8 * 16, 13 * 16)
|
||||||
|
for x in range(16, 32)}
|
||||||
|
expected.update({(x, y): green for y in range(13 * 16, 14 * 16)
|
||||||
|
for x in range(16, 32)})
|
||||||
|
snapshot(test, client, expected, 16)
|
||||||
|
|
||||||
|
# One placement is clipped and the other is discarded entirely.
|
||||||
|
test.resize(client, 40, 10)
|
||||||
|
snapshot(test, client, {(x, y): red for y in range(8 * 16, 10 * 16)
|
||||||
|
for x in range(16, 32)}, 10)
|
||||||
|
|
||||||
|
# Discarded rows must not keep the remaining placement fixed.
|
||||||
|
test.emit(b'\033[8;10r\033[S\033[r')
|
||||||
|
snapshot(test, client, {(x, y): red for y in range(7 * 16, 9 * 16)
|
||||||
|
for x in range(16, 32)}, 10)
|
||||||
|
PY
|
||||||
@@ -10,25 +10,31 @@ export TERM
|
|||||||
|
|
||||||
python3 -B - "$TEST_TMUX" <<'PY'
|
python3 -B - "$TEST_TMUX" <<'PY'
|
||||||
import sys
|
import sys
|
||||||
from image_test import image_test, sixel
|
from image_test import image_test, kitty, sixel
|
||||||
|
|
||||||
levels = [0, 14, 29, 43, 57, 71, 86, 100]
|
levels = [0, 14, 29, 43, 57, 71, 86, 100]
|
||||||
palette = [(r, g, b) for r in levels for g in levels for b in levels]
|
palette = [(r, g, b) for r in levels for g in levels for b in levels]
|
||||||
expected = [tuple((c * 255 + 50) // 100 for c in p) for p in palette]
|
expected = [tuple((c * 255 + 50) // 100 for c in p) for p in palette]
|
||||||
with image_test(sys.argv[1]) as test:
|
raw = bytes(c for rgb in expected for c in (*rgb, 255))
|
||||||
|
commands = [sixel(32, 16, palette, list(range(512))),
|
||||||
|
kitty('a=T,i=7,f=32,s=32,v=16,C=1', raw)]
|
||||||
|
for command in commands:
|
||||||
|
with image_test(sys.argv[1]) as test:
|
||||||
client = test.attach()
|
client = test.attach()
|
||||||
test.emit(sixel(32, 16, palette, list(range(512))))
|
test.emit(command)
|
||||||
images = client.images()
|
images = client.images()
|
||||||
assert len(images) == 1 and images[0][:2] == (32, 16)
|
assert len(images) == 1 and images[0][:2] == (32, 16)
|
||||||
actual = images[0][2]
|
actual = images[0][2]
|
||||||
assert all(c is not None for c in actual), 'dithering introduced holes'
|
assert all(c is not None for c in actual), 'dithering introduced holes'
|
||||||
assert 128 <= len(set(actual)) <= 256, len(set(actual))
|
assert 128 <= len(set(actual)) <= 256, len(set(actual))
|
||||||
errors = [abs(a - e) for p, q in zip(actual, expected) for a, e in zip(p, q)]
|
errors = [abs(a - e) for p, q in zip(actual, expected)
|
||||||
|
for a, e in zip(p, q)]
|
||||||
assert max(errors) <= 80, max(errors)
|
assert max(errors) <= 80, max(errors)
|
||||||
assert sum(errors) / len(errors) <= 16, sum(errors) / len(errors)
|
assert sum(errors) / len(errors) <= 16, sum(errors) / len(errors)
|
||||||
assert max(actual[0]) <= 30 and min(actual[-1]) >= 225, (
|
assert max(actual[0]) <= 30 and min(actual[-1]) >= 225, (
|
||||||
'black/white extremes lost', actual[0], actual[-1])
|
'black/white extremes lost', actual[0], actual[-1])
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
test.redraw()
|
test.redraw()
|
||||||
assert client.images() == images, 'palette or dithering changed on redraw'
|
assert client.images() == images, \
|
||||||
|
'palette or dithering changed on redraw'
|
||||||
PY
|
PY
|
||||||
|
|||||||
@@ -68,6 +68,20 @@ class Terminal:
|
|||||||
self.pixels.update(moved)
|
self.pixels.update(moved)
|
||||||
self.cells.update(cells)
|
self.cells.update(cells)
|
||||||
|
|
||||||
|
def shift(self, x, y, n):
|
||||||
|
"""Move pixels and text when characters are inserted or deleted."""
|
||||||
|
moved = {}
|
||||||
|
for (px, py), colour in list(self.pixels.items()):
|
||||||
|
if x * self.cw <= px < self.sx * self.cw and \
|
||||||
|
y * self.ch <= py < (y + 1) * self.ch:
|
||||||
|
if x * self.cw <= px - n * self.cw < self.sx * self.cw:
|
||||||
|
moved[px - n * self.cw, py] = colour
|
||||||
|
cells = {(cx - n, cy): cell for (cx, cy), cell in self.cells.items()
|
||||||
|
if cy == y and x <= cx < self.sx and x <= cx - n < self.sx}
|
||||||
|
self.erase(x, y, self.sx - x, 1)
|
||||||
|
self.pixels.update(moved)
|
||||||
|
self.cells.update(cells)
|
||||||
|
|
||||||
def csi(self, params, command):
|
def csi(self, params, command):
|
||||||
"""Apply cursor movement and erases; ignore terminal mode controls."""
|
"""Apply cursor movement and erases; ignore terminal mode controls."""
|
||||||
if params.startswith((b'?', b'>', b'=')) or b' ' in params:
|
if params.startswith((b'?', b'>', b'=')) or b' ' in params:
|
||||||
@@ -88,6 +102,8 @@ class Terminal:
|
|||||||
self.y = max(0, self.y + dy)
|
self.y = max(0, self.y + dy)
|
||||||
elif command == ord('X'):
|
elif command == ord('X'):
|
||||||
self.erase(self.x, self.y, n, 1)
|
self.erase(self.x, self.y, n, 1)
|
||||||
|
elif command in b'@P':
|
||||||
|
self.shift(self.x, self.y, n if command == ord('P') else -n)
|
||||||
elif command == ord('K'):
|
elif command == ord('K'):
|
||||||
start = self.x if args[0] == 0 else 0
|
start = self.x if args[0] == 0 else 0
|
||||||
end = self.x + 1 if args[0] == 1 else self.sx
|
end = self.x + 1 if args[0] == 1 else self.sx
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Helpers for image tests using an attached client on a PTY."""
|
"""Helpers for image tests using an attached client on a PTY."""
|
||||||
|
|
||||||
|
import base64
|
||||||
import contextlib
|
import contextlib
|
||||||
import colorsys
|
import colorsys
|
||||||
import errno
|
import errno
|
||||||
@@ -14,6 +15,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import termios
|
import termios
|
||||||
import time
|
import time
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
@@ -212,6 +214,26 @@ def image_test(tmux, sx=40, sy=16):
|
|||||||
test.close()
|
test.close()
|
||||||
|
|
||||||
|
|
||||||
|
def kitty(control, data=b''):
|
||||||
|
"""Build a Kitty graphics APC with replies suppressed."""
|
||||||
|
return b'\033_Gq=2,' + control.encode() + b';' + \
|
||||||
|
base64.b64encode(data) + b'\033\\'
|
||||||
|
|
||||||
|
|
||||||
|
def png(w, h, rows, colour_type=6, chunks=()):
|
||||||
|
"""Build an eight-bit PNG from unfiltered rows and optional chunks."""
|
||||||
|
assert len(rows) == h, (len(rows), h)
|
||||||
|
parts = [(b'IHDR', struct.pack('>2I5B', w, h, 8, colour_type, 0, 0, 0))]
|
||||||
|
parts.extend(chunks)
|
||||||
|
parts.append((b'IDAT', zlib.compress(b''.join(b'\0' + row for row in rows))))
|
||||||
|
parts.append((b'IEND', b''))
|
||||||
|
out = b'\x89PNG\r\n\x1a\n'
|
||||||
|
for kind, data in parts:
|
||||||
|
out += struct.pack('>I', len(data)) + kind + data + \
|
||||||
|
struct.pack('>I', zlib.crc32(kind + data))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def sixel(w, h, palette, pixels, p2=1):
|
def sixel(w, h, palette, pixels, p2=1):
|
||||||
"""Build SIXEL input from a palette and row-major palette indexes."""
|
"""Build SIXEL input from a palette and row-major palette indexes."""
|
||||||
assert len(pixels) == w * h
|
assert len(pixels) == w * h
|
||||||
|
|||||||
@@ -1648,6 +1648,11 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
|
|||||||
screen_write_initctx(ctx, &ttyctx, 1, 1);
|
screen_write_initctx(ctx, &ttyctx, 1, 1);
|
||||||
ttyctx.bg = bg;
|
ttyctx.bg = bg;
|
||||||
|
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_redraw_area(ctx, 0, s->cy, screen_size_x(s),
|
||||||
|
s->rlower + 1 - s->cy);
|
||||||
|
#endif
|
||||||
|
|
||||||
grid_view_insert_lines_region(gd, s->rlower, s->cy, ny, bg);
|
grid_view_insert_lines_region(gd, s->rlower, s->cy, ny, bg);
|
||||||
|
|
||||||
screen_write_collect_flush(ctx, 0, __func__);
|
screen_write_collect_flush(ctx, 0, __func__);
|
||||||
@@ -1687,6 +1692,10 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
|
|||||||
screen_write_initctx(ctx, &ttyctx, 1, 1);
|
screen_write_initctx(ctx, &ttyctx, 1, 1);
|
||||||
ttyctx.bg = bg;
|
ttyctx.bg = bg;
|
||||||
|
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), ry);
|
||||||
|
#endif
|
||||||
|
|
||||||
grid_view_delete_lines_region(gd, s->rlower, s->cy, ny, bg);
|
grid_view_delete_lines_region(gd, s->rlower, s->cy, ny, bg);
|
||||||
|
|
||||||
screen_write_collect_flush(ctx, 0, __func__);
|
screen_write_collect_flush(ctx, 0, __func__);
|
||||||
@@ -2182,8 +2191,15 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
|
|||||||
ctx->wp != NULL &&
|
ctx->wp != NULL &&
|
||||||
options_get_number(ctx->wp->options, "scroll-on-clear"))
|
options_get_number(ctx->wp->options, "scroll-on-clear"))
|
||||||
grid_view_clear_history(s->grid, bg);
|
grid_view_clear_history(s->grid, bg);
|
||||||
else
|
else {
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
for (y = 0; y < sy; y++) {
|
||||||
|
image_grid_free_line(s->grid,
|
||||||
|
&s->grid->linedata[s->grid->hsize + y]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
grid_view_clear(s->grid, 0, 0, sx, sy, bg);
|
grid_view_clear(s->grid, 0, 0, sx, sy, bg);
|
||||||
|
}
|
||||||
|
|
||||||
screen_write_collect_clear(ctx, 0, sy);
|
screen_write_collect_clear(ctx, 0, sy);
|
||||||
|
|
||||||
|
|||||||
8
screen.c
8
screen.c
@@ -120,6 +120,7 @@ screen_reinit(struct screen *s)
|
|||||||
s->saved_cy = UINT_MAX;
|
s->saved_cy = UINT_MAX;
|
||||||
|
|
||||||
screen_reset_tabs(s);
|
screen_reset_tabs(s);
|
||||||
|
grid_free_lines(s->grid, s->grid->hsize, s->grid->sy);
|
||||||
grid_clear_lines(s->grid, s->grid->hsize, s->grid->sy, 8);
|
grid_clear_lines(s->grid, s->grid->hsize, s->grid->sy, 8);
|
||||||
|
|
||||||
screen_clear_selection(s);
|
screen_clear_selection(s);
|
||||||
@@ -406,6 +407,12 @@ screen_resize_y(struct screen *s, u_int sy, int eat_empty, u_int *cy)
|
|||||||
if (available > 0) {
|
if (available > 0) {
|
||||||
if (available > needed)
|
if (available > needed)
|
||||||
available = needed;
|
available = needed;
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
for (i = oldy - available; i < oldy; i++) {
|
||||||
|
image_grid_free_line(gd,
|
||||||
|
&gd->linedata[gd->hsize + i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
grid_view_delete_lines(gd, oldy - available,
|
grid_view_delete_lines(gd, oldy - available,
|
||||||
available, 8);
|
available, 8);
|
||||||
}
|
}
|
||||||
@@ -674,6 +681,7 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
|
|||||||
}
|
}
|
||||||
memcpy(&s->saved_cell, gc, sizeof s->saved_cell);
|
memcpy(&s->saved_cell, gc, sizeof s->saved_cell);
|
||||||
|
|
||||||
|
grid_free_lines(s->grid, screen_hsize(s), sy);
|
||||||
grid_view_clear(s->grid, 0, 0, sx, sy, 8);
|
grid_view_clear(s->grid, 0, 0, sx, sy, 8);
|
||||||
|
|
||||||
s->saved_flags = s->grid->flags;
|
s->saved_flags = s->grid->flags;
|
||||||
|
|||||||
50
tmux-image.h
50
tmux-image.h
@@ -28,11 +28,14 @@ struct grid_line;
|
|||||||
struct image;
|
struct image;
|
||||||
struct image_backend;
|
struct image_backend;
|
||||||
struct image_line;
|
struct image_line;
|
||||||
|
struct image_saved;
|
||||||
struct image_store;
|
struct image_store;
|
||||||
|
struct input_ctx;
|
||||||
struct screen;
|
struct screen;
|
||||||
struct screen_write_ctx;
|
struct screen_write_ctx;
|
||||||
struct sixel_image;
|
struct sixel_image;
|
||||||
struct tty;
|
struct tty;
|
||||||
|
struct window_pane;
|
||||||
|
|
||||||
/* Maximum image size in bytes. */
|
/* Maximum image size in bytes. */
|
||||||
#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024)
|
#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024)
|
||||||
@@ -43,6 +46,12 @@ struct tty;
|
|||||||
/* Backend images move with terminal scrolling. */
|
/* Backend images move with terminal scrolling. */
|
||||||
#define IMAGE_BACKEND_SCROLLS 0x2
|
#define IMAGE_BACKEND_SCROLLS 0x2
|
||||||
|
|
||||||
|
/* Type of image. */
|
||||||
|
enum image_type {
|
||||||
|
IMAGE_TYPE_SIXEL,
|
||||||
|
IMAGE_TYPE_KITTY
|
||||||
|
};
|
||||||
|
|
||||||
/* Source image and placement geometry. */
|
/* Source image and placement geometry. */
|
||||||
struct image_geom {
|
struct image_geom {
|
||||||
/* Position and size within the source RGBA pixels. */
|
/* Position and size within the source RGBA pixels. */
|
||||||
@@ -95,11 +104,14 @@ struct image_rect {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* image.c */
|
/* image.c */
|
||||||
struct image *image_create(struct image_geom *, u_char *);
|
struct image *image_create(enum image_type, struct image_geom *, u_char *);
|
||||||
struct image *image_create_with_parent(struct image *, struct image_geom *,
|
struct image *image_create_with_parent(enum image_type, struct image *,
|
||||||
size_t, u_char *);
|
struct image_geom *, size_t, u_char *);
|
||||||
struct image *image_find(u_int);
|
struct image *image_find(u_int);
|
||||||
u_int image_get_id(struct image *);
|
u_int image_get_id(struct image *);
|
||||||
|
u_int image_get_order_id(struct image *);
|
||||||
|
void image_set_order_id(struct image *, u_int);
|
||||||
|
enum image_type image_get_type(struct image *);
|
||||||
void image_ref(struct image *);
|
void image_ref(struct image *);
|
||||||
void image_free(struct image *);
|
void image_free(struct image *);
|
||||||
void image_touch(struct image *);
|
void image_touch(struct image *);
|
||||||
@@ -113,21 +125,35 @@ void image_size_in_cells(u_int, u_int, u_int, u_int, u_int *,
|
|||||||
void image_get_pixel_area(struct image *, u_int, u_int, u_int,
|
void image_get_pixel_area(struct image *, u_int, u_int, u_int,
|
||||||
u_int, u_int *, u_int *, u_int *, u_int *);
|
u_int, u_int *, u_int *, u_int *, u_int *);
|
||||||
|
|
||||||
|
/* image-decode.c */
|
||||||
|
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, int, size_t,
|
||||||
|
u_int *, u_int *);
|
||||||
|
u_char *image_raw_decode(const u_char *, size_t, size_t, u_int, u_int,
|
||||||
|
u_int, int);
|
||||||
|
|
||||||
/* image-grid.c */
|
/* image-grid.c */
|
||||||
void image_grid_add(struct grid *, struct image *, u_int, u_int,
|
void image_grid_add(struct grid *, struct image *, u_int, u_int,
|
||||||
u_int, u_int, u_int, u_int);
|
u_int, u_int, u_int, u_int);
|
||||||
void image_grid_remove(struct grid *, u_int, u_int, u_int, u_int);
|
void image_grid_overwrite(struct grid *, u_int, u_int, u_int,
|
||||||
|
u_int);
|
||||||
void image_grid_free_line(struct grid *, struct grid_line *);
|
void image_grid_free_line(struct grid *, struct grid_line *);
|
||||||
void image_grid_free(struct grid *);
|
void image_grid_free(struct grid *);
|
||||||
void image_grid_remove_all(struct image *);
|
void image_grid_remove_all(struct image *);
|
||||||
void image_grid_move_cells(struct grid *, u_int, u_int, u_int,
|
void image_grid_move_cells(struct grid *, u_int, u_int, u_int,
|
||||||
u_int);
|
u_int);
|
||||||
|
struct image_saved *image_grid_save_lines(struct grid *, u_int, u_int, u_int,
|
||||||
|
int);
|
||||||
|
void image_grid_restore_lines(struct grid *, struct image_saved *);
|
||||||
void image_grid_duplicate_lines(struct grid *, u_int, struct grid *,
|
void image_grid_duplicate_lines(struct grid *, u_int, struct grid *,
|
||||||
u_int, u_int);
|
u_int, u_int);
|
||||||
void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *,
|
void image_grid_copy_area(struct grid *, u_int, u_int,
|
||||||
u_int, u_int, u_int, u_int);
|
struct grid *, u_int, u_int, u_int, u_int);
|
||||||
void image_grid_resize_width(struct grid *, u_int);
|
void image_grid_resize_width(struct grid *, u_int);
|
||||||
int image_grid_line_has_images(struct grid_line *);
|
int image_grid_line_has_images(struct grid_line *);
|
||||||
|
int image_grid_has_image(struct grid *, u_int);
|
||||||
|
u_int image_grid_clear(struct grid *, u_int);
|
||||||
|
u_int image_grid_clear_visible(struct grid *, enum image_type);
|
||||||
int image_grid_area_has_images(struct grid *, u_int, u_int, u_int,
|
int image_grid_area_has_images(struct grid *, u_int, u_int, u_int,
|
||||||
u_int);
|
u_int);
|
||||||
void image_redraw_area(struct screen_write_ctx *, u_int, u_int,
|
void image_redraw_area(struct screen_write_ctx *, u_int, u_int,
|
||||||
@@ -136,7 +162,9 @@ void image_redraw_all(struct screen_write_ctx *);
|
|||||||
void image_redraw_scroll(struct screen_write_ctx *);
|
void image_redraw_scroll(struct screen_write_ctx *);
|
||||||
void image_damage_screen(struct screen_write_ctx *, u_int, u_int,
|
void image_damage_screen(struct screen_write_ctx *, u_int, u_int,
|
||||||
u_int, u_int);
|
u_int, u_int);
|
||||||
void image_write(struct screen_write_ctx *, struct image *, u_int);
|
void image_write(struct screen_write_ctx *, struct image *, u_int,
|
||||||
|
int);
|
||||||
|
void image_clear(struct screen_write_ctx *, u_int);
|
||||||
int image_grid_get_at(struct grid *, u_int, u_int, struct image **,
|
int image_grid_get_at(struct grid *, u_int, u_int, struct image **,
|
||||||
u_int *, u_int *);
|
u_int *, u_int *);
|
||||||
const struct grid_cell *image_map_cell(struct tty *, struct grid *, u_int,
|
const struct grid_cell *image_map_cell(struct tty *, struct grid *, u_int,
|
||||||
@@ -168,6 +196,14 @@ void sixel_start_draw(struct tty *, u_int, u_int, u_int, u_int);
|
|||||||
void sixel_free_pending(struct tty *);
|
void sixel_free_pending(struct tty *);
|
||||||
void sixel_flush_pending(struct tty *);
|
void sixel_flush_pending(struct tty *);
|
||||||
|
|
||||||
|
/* image-kitty.c */
|
||||||
|
int kitty_select(struct tty *);
|
||||||
|
void kitty_parse_input(struct input_ctx *, struct window_pane *,
|
||||||
|
struct screen_write_ctx *, u_int, void **, const u_char *,
|
||||||
|
size_t);
|
||||||
|
void kitty_free_state(void *);
|
||||||
|
void kitty_remove_image(struct image *);
|
||||||
|
|
||||||
#endif /* ENABLE_IMAGES */
|
#endif /* ENABLE_IMAGES */
|
||||||
|
|
||||||
#endif /* TMUX_IMAGE_H */
|
#endif /* TMUX_IMAGE_H */
|
||||||
|
|||||||
2
tmux.h
2
tmux.h
@@ -3464,6 +3464,8 @@ void input_parse_pane(struct window_pane *);
|
|||||||
void input_parse_buffer(struct window_pane *, const u_char *, size_t);
|
void input_parse_buffer(struct window_pane *, const u_char *, size_t);
|
||||||
void input_parse_screen(struct input_ctx *, struct screen *,
|
void input_parse_screen(struct input_ctx *, struct screen *,
|
||||||
screen_write_init_ctx_cb, void *, const u_char *, size_t);
|
screen_write_init_ctx_cb, void *, const u_char *, size_t);
|
||||||
|
void input_reply(struct input_ctx *, int, const char *,
|
||||||
|
...) printflike(3, 4);
|
||||||
void input_reply_clipboard(struct bufferevent *, const char *, size_t,
|
void input_reply_clipboard(struct bufferevent *, const char *, size_t,
|
||||||
const char *, char);
|
const char *, char);
|
||||||
void input_set_buffer_size(size_t);
|
void input_set_buffer_size(size_t);
|
||||||
|
|||||||
Reference in New Issue
Block a user