Merge image_merge into 4902-image-support

Keep retained indexed SIXEL data and the bounded per-terminal output cache.
Use the cleaned-up decoder and RGBA encoder from image_merge, including
parser memory limits, pixel-size handling and references for pending output.
Match retained SIXEL scaling to the shared image cell footprint and emit
black definitions for undefined colour registers.

Keep Kitty input, output, layers and placeholders. Adapt backend selection,
flags, drawing and cleanup to the callbacks and Iscrl capability. Keep the
pane-status repaint with the requested XXX comment.

Replace the elaborate fallback with image_merge's implementation, removing
its caches, sample structures, quadrant/sextant features and ACS additions.
Keep the image_merge regressions and adapt the Kitty tests to the simpler
fallback. Use skip status 101 and disable Python bytecode for image tests.

Small changes still outside image_merge:
- Guard zero-height synchronized redraws before bit_nset.
- Select the image backend when the tty opens.
- Enable imagescroll by default for Kitty and Ghostty.
- Keep the explanatory manual-window pixel-size comment (comment only).
- Keep the additional SIXEL fixtures and visual test tools.

Validation: image-enabled and disabled builds; 31 image and 13 related
regressions pass normally and under ASAN. All image tests skip without
images. Focused palette, parser, scaling, transparency and Kitty layer
tests also pass after the retained-output adjustments. Diff checks clean.
This commit is contained in:
Nicholas Marriott
2026-10-14 14:28:22 +01:00
57 changed files with 2722 additions and 2295 deletions

View File

@@ -79,7 +79,7 @@ jobs:
- name: build - name: build
run: | run: |
sh autogen.sh sh autogen.sh
./configure ${{ matrix.configure }} ./configure --enable-images ${{ matrix.configure }}
${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)"
- name: test - name: test

View File

@@ -18,490 +18,161 @@
#include <sys/types.h> #include <sys/types.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "tmux.h" #include "tmux.h"
/* Character-cell fallback for clients without a graphical image protocol. */ /* UTF-8 characters used for image fallback. */
static const struct utf8_data fallback_shades[5] = {
enum image_glyph_detail { { " ", 1, 1, 1 },
IMAGE_GLYPH_ASCII, { "\342\226\221", 3, 3, 1 },
IMAGE_GLYPH_SHADE5, { "\342\226\222", 3, 3, 1 },
IMAGE_GLYPH_SHADE8, { "\342\226\223", 3, 3, 1 },
IMAGE_GLYPH_HALF, { "\342\226\210", 3, 3, 1 }
IMAGE_GLYPH_QUADRANT, };
IMAGE_GLYPH_SEXTANT static const struct utf8_data fallback_half_lower = {
"\342\226\204", 3, 3, 1
}; };
enum image_glyph_palette { /* Fallback output is available on every terminal. */
IMAGE_GLYPH_PALETTE_8, int
IMAGE_GLYPH_PALETTE_16, fallback_select(__unused struct tty *tty)
IMAGE_GLYPH_PALETTE_256,
IMAGE_GLYPH_PALETTE_RGB
};
struct image_rgb {
u_char r;
u_char g;
u_char b;
};
/* Shading levels cached for the lifetime of an image. */
struct image_glyph_data {
u_char *shade5; /* Cached levels for five-shade output. */
u_char *shade8; /* Cached levels for eight-shade output. */
};
static const struct image_rgb image_ansi_colours[16] = {
{ 0x00, 0x00, 0x00 }, { 0x80, 0x00, 0x00 },
{ 0x00, 0x80, 0x00 }, { 0x80, 0x80, 0x00 },
{ 0x00, 0x00, 0x80 }, { 0x80, 0x00, 0x80 },
{ 0x00, 0x80, 0x80 }, { 0xc0, 0xc0, 0xc0 },
{ 0x80, 0x80, 0x80 }, { 0xff, 0x00, 0x00 },
{ 0x00, 0xff, 0x00 }, { 0xff, 0xff, 0x00 },
{ 0x00, 0x00, 0xff }, { 0xff, 0x00, 0xff },
{ 0x00, 0xff, 0xff }, { 0xff, 0xff, 0xff }
};
/* Return the squared RGB distance between two colours. */
static u_int
fallback_distance(struct image_rgb a, struct image_rgb b)
{ {
int r = a.r - b.r, g = a.g - b.g, bl = a.b - b.b;
return (r * r + g * g + bl * bl);
}
/* Return whether a colour is close enough to grey to use neutral colours. */
static int
fallback_low_saturation(struct image_rgb colour)
{
u_int minimum, maximum;
minimum = maximum = colour.r;
if (colour.g < minimum)
minimum = colour.g;
if (colour.b < minimum)
minimum = colour.b;
if (colour.g > maximum)
maximum = colour.g;
if (colour.b > maximum)
maximum = colour.b;
return (maximum == 0 || (maximum - minimum) * 4 <= maximum);
}
/* Find the closest ANSI colour, keeping greys on the neutral ramp. */
static u_int
fallback_nearest_ansi(struct image_rgb colour, u_int colours)
{
u_int i, distance, best_distance = UINT_MAX, best = 0;
int neutral = fallback_low_saturation(colour);
for (i = 0; i < colours; i++) {
if (neutral && i != 0 && i != 7 &&
(colours != 16 || (i != 8 && i != 15))) {
continue;
}
distance = fallback_distance(colour, image_ansi_colours[i]);
if (distance < best_distance) {
best_distance = distance;
best = i;
}
}
return (best);
}
/* Map an RGB colour to the terminal palette and its output colour code. */
static struct image_rgb
fallback_quantize(struct image_rgb colour, enum image_glyph_palette palette,
int *output)
{
struct image_rgb result;
int value;
u_int index, ncolours;
if (palette == IMAGE_GLYPH_PALETTE_RGB) {
*output = colour_join_rgb(colour.r, colour.g, colour.b);
return (colour);
}
if (palette == IMAGE_GLYPH_PALETTE_256) {
*output = colour_find_rgb(colour.r, colour.g, colour.b);
value = colour_256toRGB(*output);
colour_split_rgb(value, &result.r, &result.g, &result.b);
return (result);
}
ncolours = 16;
if (palette == IMAGE_GLYPH_PALETTE_8)
ncolours = 8;
index = fallback_nearest_ansi(colour, ncolours);
result = image_ansi_colours[index];
if (index < 8)
*output = index;
else
*output = 90 + index - 8;
return (result);
}
/* Fit two colours to a block glyph's pixel samples. */
static void
fallback_fit_colours(const struct image_rgb *samples, u_int count,
struct image_rgb centres[2])
{
u_int sum[2][3], counts[2], group, i, j, iteration, distance;
u_int maximum = 0, first = 0, second = 0;
/* Start with the two most widely separated samples. */
for (i = 0; i < count; i++) {
for (j = i + 1; j < count; j++) {
distance = fallback_distance(samples[i], samples[j]);
if (distance > maximum) {
maximum = distance;
first = i;
second = j;
}
}
}
centres[0] = samples[first];
centres[1] = samples[second];
/* Refine the foreground and background by averaging each cluster. */
for (iteration = 0; iteration < 4; iteration++) {
memset(sum, 0, sizeof sum);
memset(counts, 0, sizeof counts);
for (i = 0; i < count; i++) {
group = fallback_distance(samples[i], centres[1]) <
fallback_distance(samples[i], centres[0]);
sum[group][0] += samples[i].r;
sum[group][1] += samples[i].g;
sum[group][2] += samples[i].b;
counts[group]++;
}
for (i = 0; i < 2; i++) {
if (counts[i] == 0)
continue;
centres[i].r = (sum[i][0] + counts[i] / 2) / counts[i];
centres[i].g = (sum[i][1] + counts[i] / 2) / counts[i];
centres[i].b = (sum[i][2] + counts[i] / 2) / counts[i];
}
}
}
/* Copy the UTF-8 character for an ACS key into a fallback cell. */
static int
fallback_set_acs(struct tty *tty, struct utf8_data *data, u_char key)
{
struct utf8_data *ud;
const char *s = tty_acs_get(tty, key);
if (s == NULL)
return (0);
ud = utf8_fromcstr(s);
if (ud[0].size == 0) {
free(ud);
return (0);
}
memcpy(data, &ud[0], sizeof *data);
free(ud);
return (1); return (1);
} }
/* Map a block's foreground mask to a half, quadrant or sextant glyph. */ /* Text images move with terminal scrolling. */
static u_char int
fallback_block_key(enum image_glyph_detail detail, u_int mask) fallback_get_flags(__unused struct tty *tty)
{ {
static const u_char half[4] = { return (IMAGE_BACKEND_SCROLLS);
0, TTY_ACS_IMAGE_HALF_UPPER, TTY_ACS_IMAGE_HALF_LOWER,
TTY_ACS_IMAGE_BLOCK
};
static const u_char quadrant[16] = {
0, TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, TTY_ACS_IMAGE_HALF_UPPER,
TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, TTY_ACS_IMAGE_HALF_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT,
TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_HALF_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT,
TTY_ACS_IMAGE_HALF_LOWER,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_BLOCK
};
u_char key;
if (detail == IMAGE_GLYPH_HALF)
return (half[mask]);
if (detail == IMAGE_GLYPH_QUADRANT)
return (quadrant[mask]);
if (mask == 0)
return (0);
if (mask == 21)
return (TTY_ACS_IMAGE_HALF_LEFT);
if (mask == 42)
return (TTY_ACS_IMAGE_HALF_RIGHT);
if (mask == 63)
return (TTY_ACS_IMAGE_BLOCK);
key = tty_acs_image_sextant(mask);
return (key);
} }
/* Build and cache dithered brightness levels for a shading palette. */ /* Get the average colour and brightness of an area. */
static u_char * static int
fallback_make_shades(struct image *im, u_int levels) fallback_sample(struct image *im, uint64_t px, uint64_t py, uint64_t pw,
uint64_t ph, u_char *brightness)
{ {
struct image_glyph_data *data = image_get_fallback_data(im); const u_char *pixels, *p;
const struct image_cell *cell; uint64_t red = 0, green = 0, blue = 0, sum = 0, count;
int *values, value, represented, error; u_int w, h, cw, ch, x0, x1, y0, y1, x, y;
size_t cells, index; size_t stride;
u_int x, y, scan, level, minimum = 255, maximum = 0;
u_int sx, sy;
int reverse;
u_char *result;
if (data == NULL) { image_get_size(im, &w, &h);
data = xcalloc(1, sizeof *data); image_get_canvas_size(im, &cw, &ch);
image_set_fallback_data(im, data); pixels = image_get_pixels(im, &stride, NULL);
x0 = px * cw / pw;
x1 = ((px + 1) * cw + pw - 1) / pw;
y0 = py * ch / ph;
y1 = ((py + 1) * ch + ph - 1) / ph;
if (x1 <= x0)
x1 = x0 + 1;
if (y1 <= y0)
y1 = y0 + 1;
if (x0 >= w || y0 >= h) {
if (brightness != NULL)
*brightness = 0;
return (colour_join_rgb(0, 0, 0));
} }
image_get_size_in_cells(im, &sx, &sy); count = (uint64_t)(x1 - x0) * (y1 - y0);
result = (levels == 5 ? data->shade5 : data->shade8);
if (result != NULL)
return (result);
/* Collect cell brightness and the range used by this image. */ if (x1 > w)
cells = (size_t)sx * sy; x1 = w;
values = xcalloc(cells, sizeof *values); if (y1 > h)
result = xcalloc(cells, 1); y1 = h;
for (y = 0; y < sy; y++) { for (y = y0; y < y1; y++) {
for (x = 0; x < sx; x++) { for (x = x0; x < x1; x++) {
index = (size_t)y * sx + x; p = pixels + y * stride + x * 4;
cell = image_get_cell(im, x, y);
value = cell->whole.brightness; red += p[0] * p[3] / 255;
if ((u_int)value < minimum) green += p[1] * p[3] / 255;
minimum = value; blue += p[2] * p[3] / 255;
if ((u_int)value > maximum)
maximum = value; sum += ((2126ULL * p[0] + 7152ULL * p[1] +
values[index] = value; 722ULL * p[2]) / 10000) * p[3] / 255;
} }
} }
/* Stretch the brightness range to make all shading levels available. */ if (brightness != NULL)
if (maximum > minimum) { *brightness = sum / count;
for (index = 0; index < cells; index++) { return (colour_join_rgb(red / count, green / count, blue / count));
values[index] = (values[index] - minimum) * 255 /
(maximum - minimum);
}
}
/* Dither each row, alternating direction to distribute the error. */
for (y = 0; y < sy; y++) {
reverse = (y & 1);
for (scan = 0; scan < sx; scan++) {
x = (reverse ? sx - scan - 1 : scan);
index = (size_t)y * sx + x;
value = values[index];
if (value < 0)
value = 0;
if (value > 255)
value = 255;
level = (value * (levels - 1) + 127) / 255;
result[index] = level;
represented = level * 255 / (levels - 1);
error = value - represented;
if (!reverse && x + 1 < sx)
values[index + 1] += error * 7 / 16;
if (reverse && x != 0)
values[index - 1] += error * 7 / 16;
if (y + 1 == sy)
continue;
if (!reverse && x != 0)
values[index + sx - 1] += error * 3 / 16;
if (reverse && x + 1 < sx)
values[index + sx + 1] += error * 3 / 16;
values[index + sx] += error * 5 / 16;
if (!reverse && x + 1 < sx)
values[index + sx + 1] += error / 16;
if (reverse && x != 0)
values[index + sx - 1] += error / 16;
}
}
free(values);
/* Keep the result for subsequent redraws at the same shading level. */
if (levels == 5)
data->shade5 = result;
else
data->shade8 = result;
return (result);
} }
/* Choose the richest colour palette supported by this terminal. */ /* Set a cell to a character representing part of an image. */
static enum image_glyph_palette
fallback_get_palette(struct tty *tty)
{
int colours;
if (tty->term->flags & TERM_RGBCOLOURS)
return (IMAGE_GLYPH_PALETTE_RGB);
if (tty->term->flags & TERM_256COLOURS)
return (IMAGE_GLYPH_PALETTE_256);
colours = tty_term_number(tty->term, TTYC_COLORS);
if (colours >= 256)
return (IMAGE_GLYPH_PALETTE_256);
if (colours >= 16)
return (IMAGE_GLYPH_PALETTE_16);
return (IMAGE_GLYPH_PALETTE_8);
}
/* Choose the fallback glyph detail supported by this terminal. */
static enum image_glyph_detail
fallback_get_detail(struct tty *tty, enum image_glyph_palette palette)
{
if (tty_acs_needed(tty))
return (IMAGE_GLYPH_ASCII);
if (tty->term->flags & TERM_IMAGE_SEXTANTS)
return (IMAGE_GLYPH_SEXTANT);
if (tty->term->flags & TERM_IMAGE_QUADRANTS)
return (IMAGE_GLYPH_QUADRANT);
if (palette != IMAGE_GLYPH_PALETTE_8)
return (IMAGE_GLYPH_HALF);
if (tty_term_has(tty->term, TTYC_NOBR))
return (IMAGE_GLYPH_SHADE5);
return (IMAGE_GLYPH_SHADE8);
}
/* Render one image cell using a two-colour block glyph. */
static void static void
fallback_block(struct tty *tty, struct image *im, u_int x, u_int y,
enum image_glyph_detail detail, enum image_glyph_palette palette,
struct grid_cell *out)
{
const struct image_cell *cell = image_get_cell(im, x, y);
struct image_rgb samples[6], centres[2], quantized[2];
u_int columns, rows, sx, sy, ix, iy, i, n, mask;
u_int x0, x1, y0, y1, red, green, blue, count;
int colours[2];
u_char key;
/* Average the stored samples into the chosen glyph's subcells. */
columns = (detail == IMAGE_GLYPH_HALF ? 1 : 2);
rows = (detail == IMAGE_GLYPH_SEXTANT ? 3 : 2);
i = 0;
for (sy = 0; sy < rows; sy++) {
for (sx = 0; sx < columns; sx++) {
x0 = sx * IMAGE_SAMPLE_COLUMNS / columns;
x1 = (sx + 1) * IMAGE_SAMPLE_COLUMNS / columns;
y0 = sy * IMAGE_SAMPLE_ROWS / rows;
y1 = (sy + 1) * IMAGE_SAMPLE_ROWS / rows;
red = green = blue = count = 0;
for (iy = y0; iy < y1; iy++) {
for (ix = x0; ix < x1; ix++) {
red += cell->samples[iy][ix].red;
green += cell->samples[iy][ix].green;
blue += cell->samples[iy][ix].blue;
count++;
}
}
samples[i].r = red / count;
samples[i].g = green / count;
samples[i].b = blue / count;
i++;
}
}
/* Fit the two colours and choose which subcells use the foreground. */
n = columns * rows;
fallback_fit_colours(samples, n, centres);
quantized[0] = fallback_quantize(centres[0], palette, &colours[0]);
quantized[1] = fallback_quantize(centres[1], palette, &colours[1]);
mask = 0;
for (i = 0; i < n; i++) {
if (fallback_distance(samples[i], quantized[1]) <
fallback_distance(samples[i], quantized[0])) {
mask |= (1U << i);
}
}
key = fallback_block_key(detail, mask);
/* Use a blank cell if the terminal has no matching glyph. */
if (key == 0)
utf8_set(&out->data, ' ');
else if (!fallback_set_acs(tty, &out->data, key))
utf8_set(&out->data, ' ');
out->fg = colours[1];
out->bg = colours[0];
}
/* Render an image cell using the terminal's text and colour capabilities. */
void
fallback_get_cell(struct tty *tty, struct image *im, u_int x, u_int y, fallback_get_cell(struct tty *tty, struct image *im, u_int x, u_int y,
const struct grid_cell *gc, struct grid_cell *out) const struct grid_cell *gc, struct grid_cell *out)
{ {
static const char ascii[] = " .:-=+*#%@"; static const char a[] = " .:-=+*#%@";
static const u_char shades[5] = { 0, TTY_ACS_IMAGE_SHADE_LIGHT, static const u_char bold_shades[8] = { 0, 1, 1, 2, 2, 3, 4, 4 };
TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_DARK, u_int ch, level, nlevels, sx, sy, colours;
TTY_ACS_IMAGE_BLOCK }; u_char brightness;
static const u_char bold_shades[8] = { 0,
TTY_ACS_IMAGE_SHADE_LIGHT, TTY_ACS_IMAGE_SHADE_LIGHT,
TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_MEDIUM,
TTY_ACS_IMAGE_SHADE_DARK, TTY_ACS_IMAGE_BLOCK,
TTY_ACS_IMAGE_BLOCK };
const struct image_cell *cell;
enum image_glyph_palette palette;
enum image_glyph_detail detail;
u_char *levels, key;
u_int level = 0, nlevels, sx, sy;
memcpy(out, gc, sizeof *out); if (out != gc)
cell = image_get_cell(im, x, y); memcpy(out, gc, sizeof *out);
if (cell == NULL) { out->flags &= GRID_FLAG_SELECTED;
out->attr = 0;
out->link = 0;
image_get_size_in_cells(im, &sx, &sy);
if (x >= sx || y >= sy) {
utf8_set(&out->data, ' '); utf8_set(&out->data, ' ');
return; return;
} }
/* Select the palette and glyph detail for this terminal. */ if (tty_acs_needed(tty)) {
palette = fallback_get_palette(tty); fallback_sample(im, x, y, sx, sy, &brightness);
detail = fallback_get_detail(tty, palette); utf8_set(&out->data, a[brightness * (sizeof a - 2) / 255]);
if (detail == IMAGE_GLYPH_ASCII) {
level = cell->whole.brightness * (sizeof ascii - 2) / 255;
utf8_set(&out->data, ascii[level]);
return; return;
} }
if (detail == IMAGE_GLYPH_SHADE5 || detail == IMAGE_GLYPH_SHADE8) {
if (tty->term->flags & (TERM_RGBCOLOURS|TERM_256COLOURS))
colours = 256;
else
colours = tty_term_number(tty->term, TTYC_COLORS);
if (colours >= 16) {
out->bg = fallback_sample(im, x, y * 2, sx, sy * 2, NULL);
out->fg = fallback_sample(im, x, y * 2 + 1, sx, sy * 2, NULL);
utf8_copy(&out->data, &fallback_half_lower);
return;
}
if (tty_term_has(tty->term, TTYC_NOBR))
nlevels = 5;
else
nlevels = 8; nlevels = 8;
if (detail == IMAGE_GLYPH_SHADE5) fallback_sample(im, x, y, sx, sy, &brightness);
nlevels = 5; level = (brightness * (nlevels - 1) + 127) / 255;
levels = fallback_make_shades(im, nlevels);
image_get_size_in_cells(im, &sx, &sy); if (nlevels == 8)
level = levels[(size_t)y * sx + x]; ch = bold_shades[level];
key = (detail == IMAGE_GLYPH_SHADE5 ? shades[level] : else
bold_shades[level]); ch = level;
if (key == 0) utf8_copy(&out->data, &fallback_shades[ch]);
utf8_set(&out->data, ' ');
else if (!fallback_set_acs(tty, &out->data, key)) out->fg = 7;
utf8_set(&out->data, ' '); out->bg = 0;
out->fg = 7; out->attr &= ~GRID_ATTR_BRIGHT;
out->bg = 0; if (nlevels == 8 && (level == 2 || level == 4 || level == 7))
out->attr &= ~GRID_ATTR_BRIGHT; out->attr |= GRID_ATTR_BRIGHT;
if (detail == IMAGE_GLYPH_SHADE8 &&
(level == 2 || level == 4 || level == 7)) {
out->attr |= GRID_ATTR_BRIGHT;
}
return;
}
fallback_block(tty, im, x, y, detail, palette, out);
} }
/* Free the brightness levels cached for text image rendering. */ /* Get a text cell for an image grid cell, if there is one. */
void const struct grid_cell *
fallback_free(struct image *im) fallback_map_cell(struct tty *tty, struct grid *gd, u_int x, u_int y,
const struct grid_cell *gc, struct grid_cell *out, int *empty)
{ {
struct image_glyph_data *data = image_get_fallback_data(im); struct image *im;
u_int src_x, src_y;
int status;
if (data == NULL) status = image_grid_get_at(gd, x, y, &im, &src_x, &src_y);
return; if (status == 1) {
free(data->shade5); fallback_get_cell(tty, im, src_x, src_y, gc, out);
free(data->shade8); if (empty != NULL)
free(data); *empty = 0;
return (out);
}
if (status == -1 && empty != NULL)
*empty = 1;
return (gc);
} }

View File

@@ -27,18 +27,6 @@
#define IMAGE_Z_BELOW_BACKGROUND (INT32_MIN / 2) #define IMAGE_Z_BELOW_BACKGROUND (INT32_MIN / 2)
/* A cell-aligned part of an image to draw at a terminal position. */
struct image_rect {
struct image *image;
int64_t z;
u_int source_x; /* Origin in image cells. */
u_int source_y;
u_int sx; /* Size in cells. */
u_int sy;
u_int destination_x; /* Origin in terminal cells. */
u_int destination_y;
};
/* One contiguous row of a placement in the grid. */ /* One contiguous row of a placement in the grid. */
struct image_span { struct image_span {
u_int x; /* Owning grid column. */ u_int x; /* Owning grid column. */
@@ -74,11 +62,19 @@ struct image_placement {
u_int app_placement_id; u_int app_placement_id;
int32_t z; int32_t z;
uint64_t serial; /* Layer order. */ uint64_t serial; /* Layer order. */
u_int x;
u_int recover_from;
struct image_spans spans; struct image_spans spans;
TAILQ_ENTRY(image_placement) entry; TAILQ_ENTRY(image_placement) entry;
}; };
TAILQ_HEAD(image_placements, image_placement); TAILQ_HEAD(image_placements, image_placement);
/* One image row to extend after a pane grows. */
struct image_recover_row {
struct image_line *line;
u_int source_y;
};
/* Placements belonging to one grid. */ /* Placements belonging to one grid. */
struct image_store { struct image_store {
uint64_t next_serial; uint64_t next_serial;
@@ -104,49 +100,80 @@ struct image_placement_map {
struct image_copy_ctx { struct image_copy_ctx {
struct grid *destination; struct grid *destination;
struct image_placement_map *maps; struct image_placement_map *maps;
size_t count; /* Placement mapping count. */ u_int count; /* Placement mapping count. */
}; };
/* A backend to draw an image. */
struct image_backend { struct image_backend {
const char *name; const char *name;
int flags; int (*get_flags)(struct tty *);
int (*select)(struct tty *);
const struct grid_cell *(*map_cell)(struct tty *, struct grid *, u_int,
u_int, const struct grid_cell *, struct grid_cell *,
int *);
void (*draw_rect)(struct tty *, const struct image_rect *); void (*draw_rect)(struct tty *, const struct image_rect *);
void (*start_draw)(struct tty *, u_int, u_int, u_int, u_int);
void (*flush)(struct tty *);
void (*finish_draw)(struct tty *);
void (*free)(struct tty *, int); void (*free)(struct tty *, int);
}; };
static const struct image_backend image_backend_fallback = { /* Backend definitions. */
"fallback", IMAGE_BACKEND_SCROLLS, NULL, NULL static struct image_backend image_backend_fallback = {
"fallback",
fallback_get_flags,
fallback_select,
fallback_map_cell,
NULL,
NULL,
NULL,
NULL,
NULL
}; };
static const struct image_backend image_backend_kitty = { static struct image_backend image_backend_sixel = {
"sixel",
sixel_get_flags,
sixel_select,
NULL,
sixel_draw_rect,
sixel_redraw_start,
sixel_flush_output,
NULL,
sixel_free_output
};
/* Kitty backend. */
static struct image_backend image_backend_kitty = {
"kitty", "kitty",
IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED, kitty_get_flags,
kitty_draw_rect, kitty_free_output_state kitty_select,
}; NULL,
static const struct image_backend image_backend_sixel = { kitty_draw_rect,
"sixel", IMAGE_BACKEND_GRAPHICAL, kitty_redraw_start,
sixel_draw_rect, sixel_free_output NULL,
kitty_redraw_finish,
kitty_free_output_state
}; };
/* Find the image backend supported by a terminal. */ /* Backends in order of preference. */
static const struct image_backend * static struct image_backend *image_backends[] = {
image_tty_find_backend(struct tty *tty) &image_backend_kitty,
{ &image_backend_sixel,
if (tty->term != NULL && tty->term->flags & TERM_KITTYGRAPHICS) &image_backend_fallback
return (&image_backend_kitty); };
if (tty->term != NULL && tty->term->flags & TERM_SIXEL &&
tty->xpixel != 0 && tty->ypixel != 0) {
return (&image_backend_sixel);
}
return (&image_backend_fallback);
}
/* Update the image backend and return whether it changed. */ /* Update the image backend. */
int int
image_tty_update(struct tty *tty) image_tty_update(struct tty *tty)
{ {
const struct image_backend *backend; struct image_backend *backend;
u_int i;
backend = image_tty_find_backend(tty); for (i = 0; i < nitems(image_backends); i++) {
backend = image_backends[i];
if (backend->select(tty))
break;
}
if (tty->image_backend == backend) if (tty->image_backend == backend)
return (0); return (0);
@@ -154,76 +181,74 @@ image_tty_update(struct tty *tty)
tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED)); tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED));
tty->image_data = NULL; tty->image_data = NULL;
tty->image_backend = backend; tty->image_backend = backend;
log_debug("%s: %s image backend is %s", __func__,
tty->client->name, backend->name);
return (1); return (1);
} }
/* Prepare the image backend to replace a redraw area. */ /* Get the flags for a terminal's image backend. */
void int
image_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, image_backend_get_flags(struct tty *tty)
u_int height)
{ {
image_tty_update(tty); image_tty_update(tty);
if (tty->image_backend == &image_backend_kitty) return (tty->image_backend->get_flags(tty));
kitty_redraw_start(tty, x, y, width, height);
else if (tty->image_backend == &image_backend_sixel)
sixel_redraw_start(tty, x, y, width, height);
} }
/* Delete stale Kitty placements after their replacements have been drawn. */ /* Map an image grid cell to text for the selected backend. */
void const struct grid_cell *
image_redraw_finish(struct tty *tty) image_map_cell(struct tty *tty, struct grid *gd, u_int x, u_int y,
const struct grid_cell *gc, struct grid_cell *out, int *empty)
{ {
if (tty->image_backend == &image_backend_kitty) image_tty_update(tty);
kitty_redraw_finish(tty); if (tty->image_backend->map_cell == NULL)
return (gc);
return (tty->image_backend->map_cell(tty, gd, x, y, gc, out, empty));
} }
/* Write out any image output the backend is still holding back. */ /* Clear old image pixels before redrawing a terminal area. */
void
image_redraw_start(struct tty *tty, u_int x, u_int y, u_int w, u_int h)
{
image_tty_update(tty);
if (tty->image_backend->start_draw != NULL)
tty->image_backend->start_draw(tty, x, y, w, h);
}
/* Write pending image output to the terminal. */
void void
image_draw_flush(struct tty *tty) image_draw_flush(struct tty *tty)
{ {
image_tty_update(tty); image_tty_update(tty);
if (tty->image_backend == &image_backend_sixel) if (tty->image_backend->flush != NULL)
sixel_flush_output(tty); tty->image_backend->flush(tty);
} }
/* Return the flags for a terminal's image backend. */ /* Discard image backend state after a terminal size change. */
int
image_backend_flags(struct tty *tty)
{
int flags;
image_tty_update(tty);
flags = tty->image_backend->flags;
/* Image scrolling is granted separately from text margin support. */
if ((tty->image_backend == &image_backend_sixel ||
tty->image_backend == &image_backend_kitty) &&
(tty->term->flags & TERM_IMAGESCROLL)) {
flags |= IMAGE_BACKEND_SCROLLS;
}
return (flags);
}
/* Discard image backend state after a terminal geometry change. */
void void
image_tty_geometry_changed(struct tty *tty) image_tty_size_changed(struct tty *tty)
{ {
image_tty_update(tty); if (!image_tty_update(tty)) {
if (tty->image_backend->free != NULL) if (tty->image_backend->free != NULL)
tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED)); tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED));
}
} }
/* Free image backend state for a terminal. */ /* Free image backend state for a terminal. */
void void
image_tty_free(struct tty *tty, int send) image_tty_free(struct tty *tty, int send)
{ {
if (tty->image_backend != NULL && tty->image_backend->free != NULL) if (tty->image_backend != NULL) {
tty->image_backend->free(tty, send); if (tty->image_backend->free != NULL)
tty->image_backend = NULL; tty->image_backend->free(tty, send);
tty->image_data = NULL; tty->image_backend = NULL;
tty->image_data = NULL;
}
}
/* Finish drawing image placements for this terminal. */
void
image_redraw_finish(struct tty *tty)
{
if (tty->image_backend->finish_draw != NULL)
tty->image_backend->finish_draw(tty);
} }
/* Return the ordering band for a placement. */ /* Return the ordering band for a placement. */
@@ -362,6 +387,34 @@ image_span_free(struct image_span *span)
free(span); free(span);
} }
/* Remove the covered part of one span. */
static void
image_span_remove(struct image_span *span, u_int x, u_int end)
{
u_int span_end = span->x + span->sx;
if (span_end <= x || span->x >= end)
return;
if (span->x < x && span_end > end) {
span->sx = x - span->x;
image_span_add(span->line, span->placement, end, span_end - end,
span->source_x + end - span->x, span->source_y);
return;
}
if (span->x < x) {
span->sx = x - span->x;
return;
}
if (span_end > end) {
span->source_x += end - span->x;
span->sx = span_end - end;
span->x = end;
return;
}
image_span_free(span);
}
/* Remove a cell range from one placement or the selected input type. */ /* Remove a cell range from one placement or the selected input type. */
static void static void
image_line_remove(struct image_line *line, u_int x, u_int width, image_line_remove(struct image_line *line, u_int x, u_int width,
@@ -369,7 +422,7 @@ image_line_remove(struct image_line *line, u_int x, u_int width,
struct image_placement *only) struct image_placement *only)
{ {
struct image_span *span, *next; struct image_span *span, *next;
u_int end, span_end, right; u_int end;
if (line == NULL || width == 0) if (line == NULL || width == 0)
return; return;
@@ -389,32 +442,9 @@ image_line_remove(struct image_line *line, u_int x, u_int width,
if (span->placement->input != input) if (span->placement->input != input)
continue; continue;
} }
span_end = span->x + span->sx; image_span_remove(span, x, end);
if (span_end <= x || span->x >= end)
continue;
/* Preserve pieces on either side of the removed cell range. */
if (span->x < x && span_end > end) {
right = span_end - end;
span->sx = x - span->x;
image_span_add(line, span->placement, end, right,
span->source_x + end - span->x, span->source_y);
continue;
}
if (span->x < x) {
span->sx = x - span->x;
continue;
}
if (span_end > end) {
span->source_x += end - span->x;
span->sx = span_end - end;
span->x = end;
continue;
}
image_span_free(span);
} }
} }
/* Remove placement records which have no remaining spans. */ /* Remove placement records which have no remaining spans. */
static void static void
image_store_prune(struct image_store *store) image_store_prune(struct image_store *store)
@@ -512,6 +542,7 @@ image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py,
moves[count].sx = span_end - start; moves[count].sx = span_end - start;
moves[count].source_x = span->source_x + start - span->x; moves[count].source_x = span->source_x + start - span->x;
moves[count].source_y = span->source_y; moves[count].source_y = span->source_y;
span->placement->recover_from = 0;
count++; count++;
} }
@@ -532,7 +563,7 @@ image_copy_placement(struct image_copy_ctx *ctx,
struct image_placement *source) struct image_placement *source)
{ {
struct image_placement *placement; struct image_placement *placement;
size_t i; u_int i;
for (i = 0; i < ctx->count; i++) { for (i = 0; i < ctx->count; i++) {
if (ctx->maps[i].source == source) if (ctx->maps[i].source == source)
@@ -542,6 +573,8 @@ image_copy_placement(struct image_copy_ctx *ctx,
source->input, source->app_image_id, source->app_placement_id, source->input, source->app_image_id, source->app_placement_id,
source->z); source->z);
placement->placeholder = source->placeholder; placement->placeholder = source->placeholder;
placement->x = source->x;
placement->recover_from = source->recover_from;
ctx->maps = xreallocarray(ctx->maps, ctx->count + 1, ctx->maps = xreallocarray(ctx->maps, ctx->count + 1,
sizeof *ctx->maps); sizeof *ctx->maps);
ctx->maps[ctx->count].source = source; ctx->maps[ctx->count].source = source;
@@ -617,6 +650,7 @@ image_grid_copy_area(struct grid *dst, u_int destination_x,
span_end = end; span_end = end;
placement = image_copy_placement(&ctx, span->placement); placement = image_copy_placement(&ctx, span->placement);
placement->recover_from = 0;
image_span_add(destination_line, placement, image_span_add(destination_line, placement,
destination_x + start - source_x, span_end - start, destination_x + start - source_x, span_end - start,
span->source_x + start - span->x, span->source_y); span->source_x + start - span->x, span->source_y);
@@ -752,12 +786,12 @@ image_rect_get_coords(const struct image_rect *rectangle,
u_int *source_x, u_int *source_y, u_int *width, u_int *height, u_int *source_x, u_int *source_y, u_int *width, u_int *height,
u_int *destination_x, u_int *destination_y) u_int *destination_x, u_int *destination_y)
{ {
*source_x = rectangle->source_x; *source_x = rectangle->src_x;
*source_y = rectangle->source_y; *source_y = rectangle->src_y;
*width = rectangle->sx; *width = rectangle->sx;
*height = rectangle->sy; *height = rectangle->sy;
*destination_x = rectangle->destination_x; *destination_x = rectangle->dst_x;
*destination_y = rectangle->destination_y; *destination_y = rectangle->dst_y;
} }
/* Return the output z-index for a drawing rectangle. */ /* Return the output z-index for a drawing rectangle. */
@@ -767,20 +801,19 @@ image_rect_get_z(const struct image_rect *rectangle)
return (rectangle->z); return (rectangle->z);
} }
/* Return one for a fallback cell, minus one to continue along an image line. */ /* Get the image covering a grid cell and its position in that image. */
int int
image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, image_grid_get_at(struct grid *gd, u_int x, u_int y, struct image **im,
const struct grid_cell *gc, struct grid_cell *out) u_int *src_x, u_int *src_y)
{ {
struct image_line *line; struct image_line *line;
struct image_span *span, *found = NULL; struct image_span *span, *found = NULL;
struct image_placement *placement; struct image_placement *placement;
struct grid_cell gc;
if (image_backend_flags(tty) & IMAGE_BACKEND_GRAPHICAL || if (y >= gd->hsize + gd->sy)
y >= s->grid->sy) {
return (0); return (0);
} line = gd->linedata[y].images;
line = s->grid->linedata[s->grid->hsize + y].images;
if (line == NULL) if (line == NULL)
return (0); return (0);
TAILQ_FOREACH(span, &line->spans, line_entry) { TAILQ_FOREACH(span, &line->spans, line_entry) {
@@ -791,22 +824,20 @@ image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y,
return (-1); return (-1);
placement = found->placement; placement = found->placement;
if (placement->input == IMAGE_INPUT_KITTY && placement->z < 0) { if (placement->input == IMAGE_INPUT_KITTY && placement->z < 0) {
if (!kitty_cell_is_placeholder(gc)) { grid_get_cell(gd, x, y, &gc);
if (gc->data.size != 1) if (!kitty_cell_is_placeholder(&gc)) {
return (-1); if (gc.data.size != 1 || gc.data.data[0] != ' ')
if (gc->data.data[0] != ' ')
return (-1); return (-1);
} }
if (placement->z < IMAGE_Z_BELOW_BACKGROUND && if (placement->z < IMAGE_Z_BELOW_BACKGROUND &&
!COLOUR_DEFAULT(gc->bg)) { !COLOUR_DEFAULT(gc.bg))
return (-1); return (-1);
}
} }
fallback_get_cell(tty, placement->image, *im = placement->image;
found->source_x + x - found->x, found->source_y, gc, out); *src_x = found->source_x + x - found->x;
*src_y = found->source_y;
return (1); return (1);
} }
/* Remove one placement and all of its spans. */ /* Remove one placement and all of its spans. */
static void static void
image_remove_placement(struct image_placement *placement) image_remove_placement(struct image_placement *placement)
@@ -1007,22 +1038,22 @@ image_redraw_all(struct screen_write_ctx *ctx)
screen_size_y(ctx->s)); screen_size_y(ctx->s));
} }
/* Record image damage within the current scroll region. */ /* Record window damage for images in the scroll region. */
void void
image_redraw_scroll(struct screen_write_ctx *ctx) image_redraw_scroll(struct screen_write_ctx *ctx)
{ {
struct screen *s = ctx->s; struct screen *s = ctx->s;
struct window_pane *wp = ctx->wp; struct window_pane *wp = ctx->wp;
u_int sx = screen_size_x(s);
u_int sy = s->rlower - s->rupper + 1;
if (wp == NULL) if (wp == NULL)
return; return;
if (!image_grid_area_has_images(s->grid, 0, s->grid->hsize + s->rupper, if (!image_grid_area_has_images(s->grid, 0,
screen_size_x(s), s->rlower - s->rupper + 1)) { s->grid->hsize + s->rupper, sx, sy))
return; return;
} redraw_damage_scroll(wp->window, wp->xoff,
redraw_damage_window_scroll(wp->window, wp->xoff, wp->yoff + s->rupper, sx, sy);
wp->yoff + s->rupper, screen_size_x(s),
s->rlower - s->rupper + 1);
} }
/* Draw a clipped part of one image span. */ /* Draw a clipped part of one image span. */
@@ -1041,12 +1072,12 @@ image_draw_span(const struct image_backend *backend, struct tty *tty,
rectangle.z = (int64_t)placement->z + 1; rectangle.z = (int64_t)placement->z + 1;
else else
rectangle.z = placement->z; rectangle.z = placement->z;
rectangle.source_x = span->source_x + start - span->x; rectangle.src_x = span->source_x + start - span->x;
rectangle.source_y = span->source_y; rectangle.src_y = span->source_y;
rectangle.sx = end - start; rectangle.sx = end - start;
rectangle.sy = 1; rectangle.sy = 1;
rectangle.destination_x = atx + start - px; rectangle.dst_x = atx + start - px;
rectangle.destination_y = aty; rectangle.dst_y = aty;
backend->draw_rect(tty, &rectangle); backend->draw_rect(tty, &rectangle);
} }
@@ -1078,7 +1109,7 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py,
image_tty_update(tty); image_tty_update(tty);
backend = tty->image_backend; backend = tty->image_backend;
if (~backend->flags & IMAGE_BACKEND_GRAPHICAL) if (~backend->get_flags(tty) & IMAGE_BACKEND_GRAPHICAL)
return; return;
if (py >= s->grid->sy) if (py >= s->grid->sy)
return; return;
@@ -1131,30 +1162,28 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py,
} }
} }
/* Return whether an image cell contains at least one nontransparent pixel. */ /* Check whether an image cell has any visible pixels. */
static int static int
image_cell_has_alpha(struct image *im, u_int x, u_int y) image_cell_has_visible_pixels(struct image *im, u_int x, u_int y)
{ {
u_int px, py, sx, sy, xx, yy; u_int px, py, sx, sy, xx, yy;
const u_char *pixels; const u_char *pixels;
size_t stride; size_t stride;
image_get_pixel_rect(im, x, y, 1, 1, &px, &py, &sx, &sy); image_get_pixel_area(im, x, y, 1, 1, &px, &py, &sx, &sy);
if (sx == 0 || sy == 0) if (sx == 0 || sy == 0)
return (0); return (0);
pixels = image_get_pixels(im, &stride, NULL); pixels = image_get_pixels(im, &stride, NULL);
for (yy = py; yy < py + sy; yy++) { for (yy = py; yy < py + sy; yy++) {
for (xx = px; xx < px + sx; xx++) { for (xx = px; xx < px + sx; xx++) {
if (pixels[(size_t)yy * stride + if (pixels[yy * stride + xx * 4 + 3] != 0)
(size_t)xx * 4 + 3] != 0) {
return (1); return (1);
}
} }
} }
return (0); return (0);
} }
/* Return whether a new cell's pixels completely cover the old cell. */ /* Return whether a new cell's pixels completely cover an old cell. */
static int static int
image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old, image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old,
u_int ox, u_int oy) u_int ox, u_int oy)
@@ -1168,20 +1197,17 @@ image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old,
image_get_canvas_size(old, &ocw, &och); image_get_canvas_size(old, &ocw, &och);
image_get_size_in_cells(new, &ncx, &ncy); image_get_size_in_cells(new, &ncx, &ncy);
image_get_size_in_cells(old, &ocx, &ocy); image_get_size_in_cells(old, &ocx, &ocy);
if (ncw / ncx != ocw / ocx || nch / ncy != och / ocy) { if (ncw / ncx != ocw / ocx || nch / ncy != och / ocy)
return (0); return (0);
} image_get_pixel_area(new, nx, ny, 1, 1, &npx, &npy, &nsx, &nsy);
image_get_pixel_rect(new, nx, ny, 1, 1, &npx, &npy, &nsx, &nsy); image_get_pixel_area(old, ox, oy, 1, 1, &opx, &opy, &osx, &osy);
image_get_pixel_rect(old, ox, oy, 1, 1, &opx, &opy, &osx, &osy);
npixels = image_get_pixels(new, &nstride, NULL); npixels = image_get_pixels(new, &nstride, NULL);
opixels = image_get_pixels(old, &ostride, NULL); opixels = image_get_pixels(old, &ostride, NULL);
for (yy = 0; yy < osy; yy++) { for (yy = 0; yy < osy; yy++) {
op = opixels + (size_t)(opy + yy) * ostride + op = opixels + (opy + yy) * ostride + opx * 4;
(size_t)opx * 4;
if (yy < nsy) { if (yy < nsy) {
np = npixels + (size_t)(npy + yy) * nstride + np = npixels + (npy + yy) * nstride + npx * 4;
(size_t)npx * 4;
} }
for (xx = 0; xx < osx; xx++) { for (xx = 0; xx < osx; xx++) {
if (op[xx * 4 + 3] == 0) if (op[xx * 4 + 3] == 0)
@@ -1224,91 +1250,63 @@ image_extend_row(struct image_line *line, struct image_placement *placement,
u_int x, run; u_int x, run;
for (x = old_end; x < new_end; x += run) { for (x = old_end; x < new_end; x += run) {
if (!image_cell_has_alpha(im, x, source_y)) { if (!image_cell_has_visible_pixels(im, x, source_y)) {
run = 1; run = 1;
continue; continue;
} }
for (run = 1; x + run < new_end; run++) { for (run = 1; x + run < new_end; run++) {
if (!image_cell_has_alpha(im, x + run, source_y)) if (!image_cell_has_visible_pixels(im, x + run, source_y))
break; break;
} }
image_span_add(line, placement, cx + x, run, x, source_y); image_span_add(line, placement, cx + x, run, x, source_y);
} }
} }
/* Extend image placements to reveal clipped columns when a pane grows. */ /* Extend image rows when grid is resized. */
void void
image_grid_resize_width(struct grid *gd, u_int new_sx) image_grid_resize_width(struct grid *gd, u_int new_sx)
{ {
struct grid_line *gl; struct image_placement *plmt;
struct image_line *line;
struct image_span *span; struct image_span *span;
struct image_placement *placement; struct image_recover_row *rows;
struct image_placement **seen = NULL; u_int image_sx, image_sy, from, to;
size_t nseen, i; u_int nrows, i;
u_int row, cx, end_x, avail, image_sx, image_sy;
u_int source_y;
int found;
if (gd->images == NULL) if (gd->images == NULL)
return; return;
for (row = 0; row < gd->hsize + gd->sy; row++) { TAILQ_FOREACH(plmt, &gd->images->placements, entry) {
gl = &gd->linedata[row]; from = plmt->recover_from;
line = gl->images; if (from == 0 || plmt->x >= new_sx)
if (line == NULL) continue;
image_get_size_in_cells(plmt->image, &image_sx, &image_sy);
to = new_sx - plmt->x;
if (to > image_sx)
to = image_sx;
if (to <= from)
continue; continue;
/* Collect placements before modifying their spans. */ rows = NULL;
nseen = 0; nrows = 0;
TAILQ_FOREACH(span, &line->spans, line_entry) { TAILQ_FOREACH(span, &plmt->spans, placement_entry) {
found = 0; for (i = 0; i < nrows; i++) {
for (i = 0; i < nseen; i++) { if (rows[i].line == span->line)
if (seen[i] == span->placement) {
found = 1;
break; break;
}
}
if (!found) {
seen = xreallocarray(seen, nseen + 1,
sizeof *seen);
seen[nseen++] = span->placement;
} }
if (i != nrows)
continue;
rows = xreallocarray(rows, nrows + 1, sizeof *rows);
rows[nrows].line = span->line;
rows[nrows].source_y = span->source_y;
nrows++;
} }
for (i = 0; i < nrows; i++) {
for (i = 0; i < nseen; i++) { image_extend_row(rows[i].line, plmt, plmt->x,
placement = seen[i]; rows[i].source_y, from, to);
if (placement->placeholder)
continue;
/* Recover the origin and last visible source column. */
cx = end_x = source_y = 0;
found = 0;
TAILQ_FOREACH(span, &line->spans, line_entry) {
if (span->placement != placement)
continue;
if (!found) {
cx = span->x - span->source_x;
source_y = span->source_y;
found = 1;
}
if (span->source_x + span->sx > end_x)
end_x = span->source_x + span->sx;
}
if (!found || cx >= new_sx)
continue;
avail = new_sx - cx;
image_get_size_in_cells(placement->image, &image_sx,
&image_sy);
if (avail > image_sx)
avail = image_sx;
if (avail <= end_x)
continue;
image_extend_row(line, placement, cx, source_y, end_x,
avail);
} }
free(rows);
plmt->recover_from = (to == image_sx ? 0 : to);
} }
free(seen);
} }
/* Place an image at the cursor using the supplied input semantics. */ /* Place an image at the cursor using the supplied input semantics. */
@@ -1324,9 +1322,10 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg,
u_int cx = s->cx, cy = s->cy; u_int cx = s->cx, cy = s->cy;
u_int x, y, i, run, sx, sy, lines, origin_y = 0; u_int x, y, i, run, sx, sy, lines, origin_y = 0;
u_int hist_origin_y, region_height, remaining, chunk; u_int hist_origin_y, region_height, remaining, chunk;
u_int cursor_x = 0, cursor_rows; u_int cursor_x = 0, cursor_rows, image_sx;
image_get_size_in_cells(im, &sx, &sy); image_get_size_in_cells(im, &sx, &sy);
image_sx = sx;
if (sx > screen_size_x(s) - cx) if (sx > screen_size_x(s) - cx)
sx = screen_size_x(s) - cx; sx = screen_size_x(s) - cx;
if (sx == 0) if (sx == 0)
@@ -1368,6 +1367,10 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg,
placement = image_placement_create(gd, im, input, app_image_id, placement = image_placement_create(gd, im, input, app_image_id,
app_placement_id, z); app_placement_id, z);
if (sx < image_sx) {
placement->x = cx;
placement->recover_from = sx;
}
/* Attach scrolled-off image rows to the available history lines. */ /* Attach scrolled-off image rows to the available history lines. */
hist_origin_y = origin_y; hist_origin_y = origin_y;
@@ -1384,12 +1387,12 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg,
for (y = 0; y < sy; y++) { for (y = 0; y < sy; y++) {
line = image_line_get(&gd->linedata[gd->hsize + cy + y]); line = image_line_get(&gd->linedata[gd->hsize + cy + y]);
for (x = 0; x < sx; x += run) { for (x = 0; x < sx; x += run) {
if (!image_cell_has_alpha(im, x, origin_y + y)) { if (!image_cell_has_visible_pixels(im, x, origin_y + y)) {
run = 1; run = 1;
continue; continue;
} }
for (run = 1; x + run < sx; run++) { for (run = 1; x + run < sx; run++) {
if (!image_cell_has_alpha(im, x + run, if (!image_cell_has_visible_pixels(im, x + run,
origin_y + y)) { origin_y + y)) {
break; break;
} }

View File

@@ -26,6 +26,25 @@
#include "tmux.h" #include "tmux.h"
/* Return whether the terminal supports Kitty graphics. */
int
kitty_select(struct tty *tty)
{
return (tty->term != NULL &&
(tty->term->flags & TERM_KITTYGRAPHICS));
}
/* Get Kitty backend flags. */
int
kitty_get_flags(struct tty *tty)
{
int flags = IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED;
if (tty_term_flag(tty->term, TTYC_ISCRL))
flags |= IMAGE_BACKEND_SCROLLS;
return (flags);
}
#define KITTY_CHUNK_SIZE 3072 #define KITTY_CHUNK_SIZE 3072
static const uint32_t kitty_diacritics[] = { static const uint32_t kitty_diacritics[] = {

File diff suppressed because it is too large Load Diff

205
image.c
View File

@@ -61,8 +61,6 @@ struct image {
size_t size; size_t size;
u_char *pixels; u_char *pixels;
struct sixel_image *sixel; struct sixel_image *sixel;
struct image_cell *cells;
void *fallback_data;
RB_ENTRY(image) entry; RB_ENTRY(image) entry;
}; };
@@ -92,7 +90,7 @@ image_find(u_int id)
return (RB_FIND(images, &images, &find)); return (RB_FIND(images, &images, &find));
} }
/* Return the ID of an image. */ /* Get the ID of an image. */
u_int u_int
image_get_id(struct image *im) image_get_id(struct image *im)
{ {
@@ -145,7 +143,7 @@ image_create(u_int w, u_int h, u_int cw, u_int ch, u_int sx, u_int sy,
return (NULL); return (NULL);
if (sx > USHRT_MAX || sy > USHRT_MAX) if (sx > USHRT_MAX || sy > USHRT_MAX)
return (NULL); return (NULL);
if ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT / sizeof(struct image_cell)) if ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT)
return (NULL); return (NULL);
im = image_alloc(w, h, cw, ch, sx, sy, w * 4, pixels); im = image_alloc(w, h, cw, ch, sx, sy, w * 4, pixels);
@@ -204,8 +202,7 @@ image_create_view(struct image *src, struct image_view *view)
view->height > src->height - view->y || view->height > src->height - view->y ||
view->sx > USHRT_MAX || view->sx > USHRT_MAX ||
view->sy > USHRT_MAX || view->sy > USHRT_MAX ||
(uint64_t)view->sx * view->sy > (uint64_t)view->sx * view->sy > IMAGE_SIZE_LIMIT ||
IMAGE_SIZE_LIMIT / sizeof(struct image_cell) ||
view->canvas_xoff > view->canvas_width || view->canvas_xoff > view->canvas_width ||
view->scaled_width > view->canvas_width - view->canvas_xoff || view->scaled_width > view->canvas_width - view->canvas_xoff ||
view->canvas_yoff > view->canvas_height || view->canvas_yoff > view->canvas_height ||
@@ -260,13 +257,11 @@ image_free(struct image *im)
image_free(im->parent); image_free(im->parent);
if (im->sixel != NULL) if (im->sixel != NULL)
sixel_free(im->sixel); sixel_free(im->sixel);
fallback_free(im);
free(im->cells);
free(im); free(im);
} }
} }
/* Return image size in pixels. */ /* Get image size in pixels. */
void void
image_get_size(struct image *im, u_int *w, u_int *h) image_get_size(struct image *im, u_int *w, u_int *h)
{ {
@@ -274,7 +269,7 @@ image_get_size(struct image *im, u_int *w, u_int *h)
*h = im->height; *h = im->height;
} }
/* Return canvas size in pixels. */ /* Get canvas size in pixels. */
void void
image_get_canvas_size(struct image *im, u_int *w, u_int *h) image_get_canvas_size(struct image *im, u_int *w, u_int *h)
{ {
@@ -282,7 +277,7 @@ image_get_canvas_size(struct image *im, u_int *w, u_int *h)
*h = im->canvas_height; *h = im->canvas_height;
} }
/* Return image size in cells. */ /* Get image size in cells. */
void void
image_get_size_in_cells(struct image *im, u_int *sx, u_int *sy) image_get_size_in_cells(struct image *im, u_int *sx, u_int *sy)
{ {
@@ -290,7 +285,7 @@ image_get_size_in_cells(struct image *im, u_int *sx, u_int *sy)
*sy = im->sy; *sy = im->sy;
} }
/* Return image pixels. */ /* Get image pixels. */
const u_char * const u_char *
image_get_pixels(struct image *im, size_t *stride, size_t *size) image_get_pixels(struct image *im, size_t *stride, size_t *size)
{ {
@@ -314,85 +309,42 @@ image_size_in_cells(u_int w, u_int h, u_int xpixel, u_int ypixel, u_int *sx,
*sy = h / ypixel + (h % ypixel != 0); *sy = h / ypixel + (h % ypixel != 0);
} }
/* Average a rectangle of image pixels into one fallback sample. */ /* Find area covered by a rectangle of cells in pixels. */
static void void
image_sample(struct image *im, uint64_t sample_x, uint64_t sample_y, image_get_pixel_area(struct image *im, u_int x, u_int y, u_int w, u_int h,
uint64_t sample_columns, uint64_t sample_rows, struct image_sample *sample) u_int *px, u_int *py, u_int *pw, u_int *ph)
{ {
const u_char *pixel; u_int left, top, right, bottom;
uint64_t red = 0, green = 0, blue = 0, alpha = 0;
uint64_t brightness = 0, count = 0;
u_int x, y, x0, x1, y0, y1;
/* Map sample bounds to pixels, counting transparent canvas padding. */ *px = *py = *pw = *ph = 0;
x0 = sample_x * im->canvas_width / sample_columns;
x1 = ((sample_x + 1) * im->canvas_width + sample_columns - 1) / if (im == NULL || x >= im->sx || y >= im->sy || w == 0 || h == 0)
sample_columns;
y0 = sample_y * im->canvas_height / sample_rows;
y1 = ((sample_y + 1) * im->canvas_height + sample_rows - 1) /
sample_rows;
if (x1 <= x0)
x1 = x0 + 1;
if (y1 <= y0)
y1 = y0 + 1;
count = (uint64_t)(x1 - x0) * (y1 - y0);
if (x0 >= im->width || y0 >= im->height)
return; return;
if (x1 > im->width) if (w > im->sx - x)
x1 = im->width; w = im->sx - x;
if (y1 > im->height) if (h > im->sy - y)
y1 = im->height; h = im->sy - y;
/* Average premultiplied colour and brightness over the sample area. */ left = (uint64_t)x * im->canvas_width / im->sx;
for (y = y0; y < y1; y++) { top = (uint64_t)y * im->canvas_height / im->sy;
for (x = x0; x < x1; x++) { right = ((uint64_t)(x + w) * im->canvas_width + im->sx - 1) / im->sx;
pixel = im->pixels + y * im->stride + x * 4; bottom = ((uint64_t)(y + h) * im->canvas_height + im->sy - 1) / im->sy;
red += pixel[0] * pixel[3] / 255;
green += pixel[1] * pixel[3] / 255;
blue += pixel[2] * pixel[3] / 255;
alpha += pixel[3];
brightness += ((2126ULL * pixel[0] +
7152ULL * pixel[1] + 722ULL * pixel[2]) / 10000) *
pixel[3] / 255;
}
}
if (count == 0)
return;
sample->red = red / count;
sample->green = green / count;
sample->blue = blue / count;
sample->alpha = alpha / count;
sample->brightness = brightness / count;
}
/* Build fallback samples for every image cell. */ if (left >= im->width || top >= im->height)
static void *px = *py = 0;
image_make_cells(struct image *im) else {
{ if (right <= left)
struct image_cell *cell; right = left + 1;
uint64_t columns, rows, column, row; if (bottom <= top)
u_int x, y, sample_x, sample_y; bottom = top + 1;
if (right > im->width)
columns = (uint64_t)im->sx * IMAGE_SAMPLE_COLUMNS; right = im->width;
rows = (uint64_t)im->sy * IMAGE_SAMPLE_ROWS; if (bottom > im->height)
im->cells = xcalloc((size_t)im->sx * im->sy, sizeof *im->cells); bottom = im->height;
for (y = 0; y < im->sy; y++) { *px = left;
for (x = 0; x < im->sx; x++) { *py = top;
cell = &im->cells[(size_t)y * im->sx + x]; *pw = right - left;
image_sample(im, x, y, im->sx, im->sy, &cell->whole); *ph = bottom - top;
column = (uint64_t)x * IMAGE_SAMPLE_COLUMNS;
row = (uint64_t)y * IMAGE_SAMPLE_ROWS;
for (sample_y = 0; sample_y < IMAGE_SAMPLE_ROWS;
sample_y++) {
for (sample_x = 0;
sample_x < IMAGE_SAMPLE_COLUMNS;
sample_x++) {
image_sample(im, column + sample_x,
row + sample_y, columns, rows,
&cell->samples[sample_y][sample_x]);
}
}
}
} }
} }
@@ -403,6 +355,13 @@ image_set_no_cursor(struct image *im)
im->flags |= IMAGE_FLAG_NO_CURSOR; im->flags |= IMAGE_FLAG_NO_CURSOR;
} }
/* Return whether writing the image should leave the cursor in place. */
int
image_has_no_cursor(struct image *im)
{
return (!!(im->flags & IMAGE_FLAG_NO_CURSOR));
}
/* Return an image's original SIXEL data. */ /* Return an image's original SIXEL data. */
struct sixel_image * struct sixel_image *
image_get_sixel(struct image *im) image_get_sixel(struct image *im)
@@ -438,80 +397,6 @@ image_set_order_id(struct image *im, u_int id)
im->order_id = id; im->order_id = id;
} }
/* Return whether writing the image should leave the cursor in place. */
int
image_has_no_cursor(struct image *im)
{
return (!!(im->flags & IMAGE_FLAG_NO_CURSOR));
}
/* Return cached fallback output data. */
void *
image_get_fallback_data(struct image *im)
{
return (im->fallback_data);
}
/* Store cached fallback output data. */
void
image_set_fallback_data(struct image *im, void *data)
{
im->fallback_data = data;
}
/* Return a precomputed fallback cell sample. */
const struct image_cell *
image_get_cell(struct image *im, u_int x, u_int y)
{
if (im == NULL || x >= im->sx || y >= im->sy)
return (NULL);
if (im->cells == NULL)
image_make_cells(im);
return (&im->cells[(size_t)y * im->sx + x]);
}
/* Convert an image cell rectangle to pixel coordinates. */
void
image_get_pixel_rect(struct image *im, u_int x, u_int y,
u_int width, u_int height, u_int *px, u_int *py, u_int *pwidth,
u_int *pheight)
{
u_int left, top, right, bottom;
*px = *py = *pwidth = *pheight = 0;
if (im == NULL || x >= im->sx || y >= im->sy || width == 0 ||
height == 0) {
return;
}
if (width > im->sx - x)
width = im->sx - x;
if (height > im->sy - y)
height = im->sy - y;
left = (uint64_t)x * im->canvas_width / im->sx;
top = (uint64_t)y * im->canvas_height / im->sy;
right = ((uint64_t)(x + width) * im->canvas_width + im->sx - 1) /
im->sx;
bottom = ((uint64_t)(y + height) * im->canvas_height + im->sy - 1) /
im->sy;
if (left >= im->width || top >= im->height) {
*px = *py = 0;
return;
}
if (right <= left)
right = left + 1;
if (bottom <= top)
bottom = top + 1;
if (right > im->width)
right = im->width;
if (bottom > im->height)
bottom = im->height;
*px = left;
*py = top;
*pwidth = right - left;
*pheight = bottom - top;
}
/* Decode a base64 payload with a size limit. */ /* Decode a base64 payload with a size limit. */
u_char * u_char *
image_base64_decode(const char *data, size_t len, size_t limit, size_t *size) image_base64_decode(const char *data, size_t len, size_t limit, size_t *size)

View File

@@ -32,13 +32,18 @@ $(TESTS): prepare
@base="$@"; base=$${base##*/}; \ @base="$@"; base=$${base##*/}; \
log="$(LOGDIR)/$${base%.sh}.log"; \ log="$(LOGDIR)/$${base%.sh}.log"; \
start=$$(date +%s); \ start=$$(date +%s); \
if env -i LC_CTYPE=C.UTF-8 MallocNanoZone=0 \ env -i LC_CTYPE=C.UTF-8 MallocNanoZone=0 \
sh -x "$@" >"$$log" 2>&1; then \ sh -x "$@" >"$$log" 2>&1; \
end=$$(date +%s); \ result=$$?; \
end=$$(date +%s); \
case $$result in \
0) \
rm -f "$$log"; \ rm -f "$$log"; \
printf '%-40s PASS (%ss)\n' "$@" "$$((end - start))"; \ printf '%-40s PASS (%ss)\n' "$@" "$$((end - start))" ;; \
else \ 101) \
end=$$(date +%s); \ rm -f "$$log"; \
printf '%-40s SKIP (%ss)\n' "$@" "$$((end - start))" ;; \
*) \
printf '%-40s FAIL (%ss)\n log: %s\n' \ printf '%-40s FAIL (%ss)\n log: %s\n' \
"$@" "$$((end - start))" "$$log"; \ "$@" "$$((end - start))" "$$log" ;; \
fi esac

View File

@@ -4,6 +4,8 @@
# pane-border-status text. Exercise the same queued new; newp; send-keys path # pane-border-status text. Exercise the same queued new; newp; send-keys path
# used by the reported reproduction, then repeat it with two tiled panes. # used by the reported reproduction, then repeat it with two tiled panes.
. ./image-common.inc
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 LC_ALL=C.UTF-8
@@ -38,7 +40,7 @@ missing_titles=
trap "cleanup; rm -f $TMP" 0 1 15 trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 100 -y 30 sh || fail "new session failed" $TMUX new-session -d -s inner -x 100 -y 30 sh || fail "new session failed"
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX
$TMUX set -g status off $TMUX set -g status off
$TMUX set -g pane-border-status top $TMUX set -g pane-border-status top
$TMUX set -g pane-border-format '#{pane_title}' $TMUX set -g pane-border-format '#{pane_title}'

View File

@@ -3,6 +3,8 @@
# A tiled pane's border title must reappear after a floating pane moves away. # A tiled pane's border title must reappear after a floating pane moves away.
# Start the drag below row zero so its initial press hits the floating pane. # Start the drag below row zero so its initial press hits the floating pane.
. ./image-common.inc
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 LC_ALL=C.UTF-8
@@ -69,6 +71,7 @@ TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15 trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1 $TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
image_check_support $TMUX
$TMUX set -g mouse on $TMUX set -g mouse on
$TMUX set -g pane-border-status top $TMUX set -g pane-border-status top
$TMUX set -g default-command 'sh -c "sleep 100"' $TMUX set -g default-command 'sh -c "sleep 100"'
@@ -116,7 +119,6 @@ while [ "$row" != "$FTOP" ]; do
waypoints="$waypoints $row" waypoints="$waypoints $row"
done done
drag $((GRABCOL + 1)) $waypoints drag $((GRABCOL + 1)) $waypoints
drag $((GRABCOL + 1)) 1 $((GRABCOL + 1)) "$FTOP"
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed" $TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"

10
regress/image-common.inc Normal file
View File

@@ -0,0 +1,10 @@
# Check server image support. Exit 101 to tell the runner to skip this test.
image_check_support()
{
support=$("$@" display-message -p '#{image_support}') || exit 1
case "$support" in
1) return ;;
0) exit 101 ;;
*) echo "unexpected image_support: $support" >&2; exit 1 ;;
esac
}

108
regress/image-fallback.sh Executable file
View File

@@ -0,0 +1,108 @@
#!/bin/sh
# SIXEL input is visible as text on ASCII and UTF-8 clients, including
# copy-mode selection and a graphical client with no known cell pixel size.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import re
import sys
from image_test import image_test, sixel
from image_terminal import Terminal
white = sixel(16, 16, [(100, 100, 100)], [0] * (16 * 16))
for utf8 in (False, True):
with image_test(sys.argv[1]) as test:
client = test.attach(graphical=False, utf8=utf8)
char = b'\xe2\x96\x84' if utf8 else b'@'
test.emit(white)
assert not client.images(), 'graphical output on a fallback client'
assert client.data.count(char) >= 2, (utf8, bytes(client.data))
test.run('copy-mode')
test.run('send-keys', '-X', 'history-top')
test.run('send-keys', '-X', 'start-of-line')
test.run('send-keys', '-X', 'begin-selection')
test.run('send-keys', '-X', 'cursor-right')
test.redraw()
assert not client.images()
assert client.data.count(char) >= 2, ('selection lost fallback', utf8)
test.run('send-keys', '-X', 'cancel')
test.emit(b'\033[2J\033[H')
assert not client.images()
# Clearing must also remove the image from later full redraws.
test.redraw()
assert char not in client.data, ('cleared fallback reappeared', utf8)
with image_test(sys.argv[1]) as test:
client = test.attach(graphical=True, utf8=False, cw=0, ch=0)
test.emit(white)
assert not client.images(), 'SIXEL sent without a cell pixel size'
# Default pixel sizes may leave padding, so the brightness character
# need not be '@'. Remove terminal controls before checking the text.
text = re.sub(rb'\033\[[0-?]*[ -/]*[@-~]', b'', client.data)
text = re.sub(rb'\033\][^\007]*?(?:\007|\033\\)', b'', text)
text = re.sub(rb'\033.', b'', text)
assert any(bytes([ch]) in text for ch in b'.:-=+*#%@'), (
'missing fallback with unknown pixel size', text)
# Every ASCII brightness character is reachable, in the right grid cell.
levels = [0, 12, 23, 34, 45, 56, 67, 78, 89, 100]
with image_test(sys.argv[1]) as test:
client = test.attach(graphical=False, utf8=False)
test.emit(sixel(80, 16, [(v, v, v) for v in levels],
[x // 8 for y in range(16) for x in range(80)]))
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
actual = ''.join(terminal.cells[x, 0][0] for x in range(10))
assert actual == ' .:-=+*#%@', actual
# The lower-half character uses the upper sample as background and the
# lower sample as foreground. Check true colour and 256-colour mapping.
for truecolour in (False, True):
with image_test(sys.argv[1]) as test:
if truecolour:
test.run('set-option', '-as', 'terminal-features',
'screen-256color:RGB')
client = test.attach(graphical=False)
test.emit(b'\033[3;4H' + sixel(8, 16,
[(100, 0, 0), (0, 0, 100)],
[y >= 8 for y in range(16) for x in range(8)]))
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
char, fg, bg, bold = terminal.cells[3, 2]
assert char == '\u2584' and not bold, (char, fg, bg, bold)
if truecolour:
expected = ((0, 0, 255), (255, 0, 0))
else:
expected = (21, 196)
assert (fg, bg) == expected, (truecolour, fg, bg)
# Eight-colour UTF-8 terminals use shades, with bold supplying extra levels
# unless Nobr disables it. Force the capabilities without extra terminfo files.
for nobr in (False, True):
with image_test(sys.argv[1]) as test:
overrides = 'screen:colors=8:Nobr=' if nobr else 'screen:colors=8:Nobr@'
test.run('set-option', '-as', 'terminal-overrides', overrides)
client = test.attach(graphical=False, term='screen')
levels = [0, 14, 29, 43, 57, 71, 86, 100]
test.emit(sixel(64, 16, [(v, v, v) for v in levels],
[x // 8 for y in range(16) for x in range(64)]))
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
chars = ''.join(terminal.cells[x, 0][0] for x in range(8))
bold = [terminal.cells[x, 0][3] for x in range(8)]
if nobr:
assert chars == ' \u2591\u2591\u2592\u2592\u2593\u2593\u2588', chars
assert not any(bold), bold
else:
assert chars == ' \u2591\u2591\u2592\u2592\u2593\u2588\u2588', chars
assert bold == [False, False, True, False, True, False, False, True], bold
PY

34
regress/image-feature-redraw.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/sh
# Terminal replies which change no features must not replay our images.
# A reply which adds a feature must still cause a redraw.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import os
import sys
from image_test import image_test, sixel, check_image
with image_test(sys.argv[1]) as test:
client = test.attach()
test.emit(sixel(8, 16, [(100, 0, 0)], [0] * (8 * 16)))
assert len(client.images()) == 1
client.clear()
# The primary DA was answered during attach. This secondary DA adds
# nothing, but still calls tty_update_features().
os.write(client.fd, b'\033[>0;1;0c')
client.read(0.5)
assert not client.images(), 'unchanged features replayed the image'
features = test.run('display-message', '-p', '#{client_termfeatures}')
assert 'sync' not in features.split(','), features
os.write(client.fd, b'\033[?2026;1$y')
test.wait(lambda: 'sync' in test.run('display-message', '-p',
'#{client_termfeatures}').split(','))
client.read(0.2)
assert len(client.images()) == 1, 'new terminal feature did not redraw'
check_image(client.images()[0], 8, 16, [(255, 0, 0)] * (8 * 16))
PY

View File

@@ -3,6 +3,8 @@
# A SIXEL pane redraw must erase only cells owned by that pane, preserving # A SIXEL pane redraw must erase only cells owned by that pane, preserving
# content in an overlapping floating pane when leaving copy mode. # content in an overlapping floating pane when leaving copy mode.
. ./image-common.inc
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 LC_ALL=C.UTF-8
@@ -21,7 +23,7 @@ $TMUX2 new-session -d -x 60 -y 20 "sh -c 'i=0; while [ \$i -lt 30 ]; do printf \
|| exit 1 || exit 1
$TMUX2 new-pane -x20 -y6 -X8 -Y3 -T FLOATTITLE "sh -c 'printf FLOATCONTENT; exec sleep 100'" \ $TMUX2 new-pane -x20 -y6 -X8 -Y3 -T FLOATTITLE "sh -c 'printf FLOATCONTENT; exec sleep 100'" \
|| exit 1 || exit 1
[ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX2
# The attaching client must be detected as SIXEL-capable - this bug does # The attaching client must be detected as SIXEL-capable - this bug does
# not reproduce on a fallback or Kitty client. # not reproduce on a fallback or Kitty client.

125
regress/image-grid-operations.sh Executable file
View File

@@ -0,0 +1,125 @@
#!/bin/sh
# Check image positions and erases, line operations, scroll regions,
# history cleanup and reflow next to wrapped text.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import sys
from image_test import image_test, sixel
from image_terminal import Terminal
palette = [(x * 30, y * 40, 100) for y in range(3) for x in range(4)]
rgb = [tuple((c * 255 + 50) // 100 for c in colour) for colour in palette]
wire = sixel(32, 48, palette,
[y // 16 * 4 + x // 8 for y in range(48) for x in range(32)])
cells = {(x + 2, y + 2): rgb[y * 4 + x]
for y in range(3) for x in range(4)}
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)}
def snapshot(test, client, expected):
"""Check a full redraw independently of previous terminal contents."""
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
terminal.check(pixels(expected))
return terminal
cases = [
('erase characters', b'\033[3;4H\033[2X',
{(x, y): c for (x, y), c in cells.items() if y != 2 or x not in (3, 4)}),
('erase line end', b'\033[4;5H\033[K',
{(x, y): c for (x, y), c in cells.items() if y != 3 or x < 4}),
('erase line start', b'\033[4;4H\033[1K',
{(x, y): c for (x, y), c in cells.items() if y != 3 or x > 3}),
('erase whole line', b'\033[4;4H\033[2K',
{(x, y): c for (x, y), c in cells.items() if y != 3}),
('erase display end', b'\033[4;5H\033[J',
{(x, y): c for (x, y), c in cells.items() if y < 3 or y == 3 and x < 4}),
('erase display start', b'\033[4;5H\033[1J',
{(x, y): c for (x, y), c in cells.items() if y > 3 or y == 3 and x > 4}),
('erase whole display', b'\033[2J', {}),
('insert line', b'\033[4;1H\033[L',
{(x, y + (y >= 3)): c for (x, y), c in cells.items()}),
('delete line', b'\033[4;1H\033[M',
{(x, y - (y > 3)): c for (x, y), c in cells.items() if y != 3}),
('scroll region up', b'\033[3;6r\033[S',
{(x, y - 1): c for (x, y), c in cells.items() if y > 2}),
('scroll region down', b'\033[3;6r\033[T',
{(x, y + 1): c for (x, y), c in cells.items()}),
('reverse index', b'\033[3;6r\033[3;1H\033M',
{(x, y + 1): c for (x, y), c in cells.items()}),
]
with image_test(sys.argv[1]) as test:
client = test.attach()
for name, operation, expected in cases:
test.emit(b'\033[r\033[2J\033[3;3H' + wire)
terminal = snapshot(test, client, cells)
test.emit(operation)
if name.startswith('erase'):
# Check actual erase commands, before a full refresh can hide a bug.
terminal.feed(client.data)
terminal.check(pixels(expected))
try:
snapshot(test, client, expected)
except AssertionError as e:
raise AssertionError(name) from e
# Image lines stay whole while the surrounding text reflows.
with image_test(sys.argv[1], sx=16, sy=20) as test:
client = test.attach()
text = b'abcdefghijklmnopqrstuvwx'
small = sixel(32, 32, palette,
[y // 16 * 4 + x // 8 for y in range(32) for x in range(32)])
test.emit(text + b'\r\n' + small + text + b'\r\n')
for width in (8, 16, 8, 16):
test.resize(client, width, 20)
test.redraw()
terminal = Terminal(sx=width, sy=20)
terminal.feed(client.data)
# Reflow can move the leading text into history to retain the cursor.
top = (len(text) + width - 1) // width - int(test.run(
'display-message', '-p', '#{history_size}'))
assert top >= 0, top
expected = {(x, y + top): rgb[y * 4 + x]
for y in range(2) for x in range(4)}
terminal.check(pixels(expected))
captured = test.run('capture-pane', '-p', '-S', '-').replace('\n', '')
assert captured == text.decode() * 2, captured
# Evicted or explicitly cleared history must not leave images in copy mode.
for evict in (False, True):
with image_test(sys.argv[1], sy=6) as test:
if evict:
test.run('set-option', '-g', 'history-limit', '2')
test.run('new-window', '-k', '-t', 'image:0',
"stty raw -echo; printf '\033]2;READY2\007'; exec cat")
test.wait(lambda: test.run('display-message', '-p',
'#{pane_title}') == 'READY2')
client = test.attach()
test.emit(wire)
assert client.images(), 'image missing before scrolling into history'
test.emit(b'\033[12S')
test.run('copy-mode')
test.run('send-keys', '-X', 'history-top')
test.redraw()
if evict:
assert not client.images(), 'evicted history retained an image'
else:
assert client.images(), 'image missing before clearing history'
test.run('send-keys', '-X', 'cancel')
test.run('clear-history')
test.run('copy-mode')
test.run('send-keys', '-X', 'history-top')
test.redraw()
assert not client.images(), 'cleared history retained an image'
PY

76
regress/image-grid.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/sh
# Images follow grid movement, overwrites, clear, reflow and copy mode.
# Check output pixels so lost or incorrect source coordinates are detected.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import sys
from image_test import image_test, sixel, check_image
palette = [(100, 0, 0), (0, 100, 0), (0, 0, 100), (100, 100, 100)]
rgb = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)]
pixels = [2 * (y >= 16) + (x >= 8) for y in range(32) for x in range(16)]
wire = sixel(16, 32, palette, pixels)
with image_test(sys.argv[1]) as test:
client = test.attach()
# A scroll preserves the second source row and puts the first in history.
test.emit(wire + b'\033[1S')
assert len(client.images()) == 1
check_image(client.images()[0], 16, 16,
[rgb[2 + (x >= 8)] for y in range(16) for x in range(16)])
client.clear()
test.run('copy-mode')
test.run('send-keys', '-X', 'history-top')
client.read(0.3)
assert any(im[:2] == (16, 32) for im in client.images())
image = next(im for im in client.images() if im[:2] == (16, 32))
check_image(image, 16, 32, [rgb[c] for c in pixels])
test.run('send-keys', '-X', 'cancel')
# Insert a blank cell inside the image, then delete the gap again.
pixels = [x >= 8 for y in range(16) for x in range(16)]
test.emit(b'\033[2J\033[H' + sixel(16, 16, palette[:2], pixels))
test.emit(b'\033[1;2H\033[@')
images = client.images()
assert len(images) == 2, [im[:2] for im in images]
for im, colour in zip(images, rgb[:2]):
check_image(im, 8, 16, [colour] * (8 * 16))
test.emit(b'\033[1;2H\033[P')
images = client.images()
assert len(images) == 2, [im[:2] for im in images]
for im, colour in zip(images, rgb[:2]):
check_image(im, 8, 16, [colour] * (8 * 16))
# A double-width character overwrites both image cells.
test.emit(b'\033[H' + '界'.encode())
assert not client.images()
assert test.run('capture-pane', '-p').startswith('界')
test.redraw()
assert not client.images()
# Narrowing keeps image rows whole; widening restores the clipped column.
pixels = [2 * (y >= 16) + (x >= 8) for y in range(32) for x in range(16)]
test.emit(b'\033[2J\033[H' + wire)
test.resize(client, 1, 16)
assert len(client.images()) == 1
check_image(client.images()[0], 8, 32,
[rgb[2 * (y >= 16)] for y in range(32) for x in range(8)])
test.resize(client, 40, 16)
assert len(client.images()) == 1
check_image(client.images()[0], 16, 32, [rgb[c] for c in pixels])
# Switching the alternate screen does not retain the other screen's image.
test.emit(b'\033[?1049h')
assert not client.images()
test.redraw()
assert not client.images()
test.emit(b'\033[?1049l')
assert len(client.images()) == 1
check_image(client.images()[0], 16, 32, [rgb[c] for c in pixels])
PY

View File

@@ -19,7 +19,7 @@ $TMUX new-session -d -x 10 -y 4 "
read line read line
printf '\033[H\033[2J' printf '\033[H\033[2J'
sleep 30" || exit 1 sleep 30" || exit 1
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 [ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 101
$TMUX set -g status off || exit 1 $TMUX set -g status off || exit 1
$TMUX set -g scroll-on-clear off || exit 1 $TMUX set -g scroll-on-clear off || exit 1
$TMUX set -as terminal-features ',*:sixel@' || exit 1 $TMUX set -as terminal-features ',*:sixel@' || exit 1

View File

@@ -8,9 +8,9 @@ export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
export TEST_TMUX export TEST_TMUX
command -v python3 >/dev/null || exit 0 command -v python3 >/dev/null || exit 101
python3 - <<'PY' python3 -B - <<'PY'
import base64 import base64
import os import os
from pathlib import Path from pathlib import Path
@@ -105,9 +105,9 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-layers-') as tmp:
'time.sleep(30)\n') 'time.sleep(30)\n')
try: try:
run(inner, 'new-session', '-d', '-x', '40', '-y', '12', run(inner, 'new-session', '-d', '-x', '40', '-y', '12',
shlex.join(['python3', str(helper)])) shlex.join(['python3', '-B', str(helper)]))
if run(inner, 'display-message', '-p', '#{image_support}') == '0': if run(inner, 'display-message', '-p', '#{image_support}') == '0':
raise SystemExit(0) raise SystemExit(101)
run(inner, 'set', '-g', 'status', 'off') run(inner, 'set', '-g', 'status', 'off')
run(inner, 'set', '-as', 'terminal-features', ',*:kittygraphics') run(inner, 'set', '-as', 'terminal-features', ',*:kittygraphics')
pane = run(outer, 'new-session', '-dP', '-F', '#{pane_id}', pane = run(outer, 'new-session', '-dP', '-F', '#{pane_id}',

View File

@@ -9,9 +9,9 @@ export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
export TEST_TMUX export TEST_TMUX
command -v python3 >/dev/null || exit 0 command -v python3 >/dev/null || exit 101
python3 - <<'PY' python3 -B - <<'PY'
import base64 import base64
import json import json
import re import re
@@ -61,7 +61,7 @@ def check(name, command, cursor, expected='', text=None, render=None, resize=Non
output = directory / name output = directory / name
command_file = directory / (name + '.input') command_file = directory / (name + '.input')
command_file.write_text(command, encoding='utf-8') command_file.write_text(command, encoding='utf-8')
pane_command = 'python3 ' + shlex.quote(str(helper)) + ' ' + \ pane_command = 'python3 -B ' + shlex.quote(str(helper)) + ' ' + \
shlex.quote(str(command_file)) + ' ' + shlex.quote(str(output)) shlex.quote(str(command_file)) + ' ' + shlex.quote(str(output))
pane = run('new-window', '-d', '-P', '-F', '#{pane_id}', pane_command) pane = run('new-window', '-d', '-P', '-F', '#{pane_id}', pane_command)
try: try:
@@ -109,7 +109,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
try: try:
run('new-session', '-d', '-x', '40', '-y', '12') run('new-session', '-d', '-x', '40', '-y', '12')
if run('display-message', '-p', '#{image_support}') == '0': if run('display-message', '-p', '#{image_support}') == '0':
raise SystemExit(0) raise SystemExit(101)
run('set', '-g', 'status', 'off') run('set', '-g', 'status', 'off')
run('set', '-as', 'terminal-features', ',*:sixel@') run('set', '-as', 'terminal-features', ',*:sixel@')
run('set', '-as', 'terminal-features', ',*:kittygraphics@') run('set', '-as', 'terminal-features', ',*:kittygraphics@')
@@ -264,13 +264,13 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
high_virtual = graphics('a=T,q=2,U=1,i=16777223,f=32,s=24,v=32,c=3,r=2', white) high_virtual = graphics('a=T,q=2,U=1,i=16777223,f=32,s=24,v=32,c=3,r=2', white)
check('inherit-high-byte', high_virtual + '\033[38;2;0;0;7m' + check('inherit-high-byte', high_virtual + '\033[38;2;0;0;7m' +
ph + '\u0305\u0305\u030d' + ph, '2,0', ph + '\u0305\u0305\u030d' + ph, '2,0',
text=ph + '\u0305\u0305\u030d' + ph, render=r'48;2;255;255;255m {2}') text=ph + '\u0305\u0305\u030d' + ph, render=r'48;2;255;255;255m[ ▄]{2}')
check('inherit-high-byte-row', high_virtual + '\033[38;2;0;0;7m' + check('inherit-high-byte-row', high_virtual + '\033[38;2;0;0;7m' +
ph + '\u0305\u0305\u030d' + ph + '\u0305', '2,0', ph + '\u0305\u0305\u030d' + ph + '\u0305', '2,0',
render=r'48;2;255;255;255m {2}') render=r'48;2;255;255;255m[ ▄]{2}')
check('inherit-high-byte-column', high_virtual + '\033[38;2;0;0;7m' + check('inherit-high-byte-column', high_virtual + '\033[38;2;0;0;7m' +
ph + '\u0305\u0305\u030d' + ph + '\u0305\u030d', '2,0', ph + '\u0305\u0305\u030d' + ph + '\u0305\u030d', '2,0',
render=r'48;2;255;255;255m {2}') render=r'48;2;255;255;255m[ ▄]{2}')
red = base64.b64encode(b'\xff\0\0\xff' * (24 * 32)).decode() red = base64.b64encode(b'\xff\0\0\xff' * (24 * 32)).decode()
check('explicit-high-byte', high_virtual + check('explicit-high-byte', high_virtual +
graphics('a=T,q=2,U=1,i=7,f=32,s=24,v=32,c=3,r=2', red) + graphics('a=T,q=2,U=1,i=7,f=32,s=24,v=32,c=3,r=2', red) +
@@ -289,7 +289,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
check('negative-virtual', virtual + graphics('a=p,q=2,U=1,i=7,p=1,c=3,r=2,z=-1') + check('negative-virtual', virtual + graphics('a=p,q=2,U=1,i=7,p=1,c=3,r=2,z=-1') +
colours + ph, '1,0', render=r'255;255;255m') colours + ph, '1,0', render=r'255;255;255m')
check('placeholder-resize', virtual + colours + ph, '1,0', check('placeholder-resize', virtual + colours + ph, '1,0',
render=r'48;2;255;255;255m(?:\x1b\[[0-9;]+m)* \x1b\[(?:0|39)m', render=r'48;2;255;255;255m(?:\x1b\[[0-9;]+m)*[ ▄]\x1b\[(?:0|39)m',
resize=(50, 12)) resize=(50, 12))
for selector in ['a', 'z']: for selector in ['a', 'z']:
check('virtual-survives-' + selector, virtual + colours + ph + check('virtual-survives-' + selector, virtual + colours + ph +
@@ -301,7 +301,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
check('virtual-survives-' + selector, virtual + colours + ph + check('virtual-survives-' + selector, virtual + colours + ph +
'\033[H' + graphics('a=d,d=%s,q=2%s' % (selector, coordinates)) + '\033[H' + graphics('a=d,d=%s,q=2%s' % (selector, coordinates)) +
'\033[2G' + ph + '\u0305\u030d', '2,0', '\033[2G' + ph + '\u0305\u030d', '2,0',
render=r'48;2;255;255;255m(?:\x1b\[[0-9;]+m)* {2}') render=r'48;2;255;255;255m(?:\x1b\[[0-9;]+m)*[ ▄]{2}')
# Deleting a prototype leaves its existing text-backed display intact. # Deleting a prototype leaves its existing text-backed display intact.
check('virtual-delete-display', virtual + colours + ph + check('virtual-delete-display', virtual + colours + ph +
graphics('a=d,d=I,i=7,p=1,q=2'), '1,0', render=r'255;255;255m') graphics('a=d,d=I,i=7,p=1,q=2'), '1,0', render=r'255;255;255m')
@@ -321,7 +321,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
graphics('a=t,q=2,i=7,f=32,s=1,v=1', pixel), '1,0', text=ph, render=ph) graphics('a=t,q=2,i=7,f=32,s=1,v=1', pixel), '1,0', text=ph, render=ph)
check('virtual-replace-display', virtual + colours + ph + check('virtual-replace-display', virtual + colours + ph +
graphics('a=p,i=7,p=1,q=2,C=1,c=3,r=2'), '1,0', graphics('a=p,i=7,p=1,q=2,C=1,c=3,r=2'), '1,0',
render=r'^(?:\x1b\[[0-9;]+m)*\x1b\[48;2;255;255;255m(?:\x1b\[[0-9;]+m)* ') render=r'^(?:\x1b\[[0-9;]+m)*\x1b\[48;2;255;255;255m(?:\x1b\[[0-9;]+m)*[ ▄]')
check('placeholder-overwrite', virtual + colours + ph + '\033[HX', '1,0', check('placeholder-overwrite', virtual + colours + ph + '\033[HX', '1,0',
text='X', render=r'X') text='X', render=r'X')
check('placeholder-erase', virtual + colours + ph + '\033[H\033[2K', '0,0', check('placeholder-erase', virtual + colours + ph + '\033[H\033[2K', '0,0',
@@ -330,13 +330,13 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
# Ordinary placements stretch; placeholders retain aspect ratio. # Ordinary placements stretch; placeholders retain aspect ratio.
check('stretch-tall', graphics('a=T,q=2,f=32,s=1,v=1,c=2,r=2,C=1', check('stretch-tall', graphics('a=T,q=2,f=32,s=1,v=1,c=2,r=2,C=1',
white_pixel), white_pixel),
'0,0', render=r'48;2;255;255;255m {2}') '0,0', render=r'48;2;255;255;255m[ ▄]{2}')
check('stretch-wide', graphics('a=T,q=2,f=32,s=1,v=1,c=4,r=1,C=1', check('stretch-wide', graphics('a=T,q=2,f=32,s=1,v=1,c=4,r=1,C=1',
white_pixel), white_pixel),
'0,0', render=r'48;2;255;255;255m {4}') '0,0', render=r'48;2;255;255;255m[ ▄]{4}')
check('stretch-crop', graphics('a=T,q=2,f=32,s=24,v=32,x=4,y=8,w=8,h=8,' check('stretch-crop', graphics('a=T,q=2,f=32,s=24,v=32,x=4,y=8,w=8,h=8,'
'c=2,r=2,C=1', white), 'c=2,r=2,C=1', white),
'0,0', render=r'48;2;255;255;255m {2}') '0,0', render=r'48;2;255;255;255m[ ▄]{2}')
check('virtual-letterbox', graphics('a=T,q=2,U=1,i=7,p=1,f=32,s=1,v=1,' check('virtual-letterbox', graphics('a=T,q=2,U=1,i=7,p=1,f=32,s=1,v=1,'
'c=2,r=2', white_pixel) + colours + 'c=2,r=2', white_pixel) + colours +
ph + '\u0305\u0305' + ph + '\u0305\u030d', ph + '\u0305\u0305' + ph + '\u0305\u030d',
@@ -344,24 +344,24 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
check('virtual-pillarbox', graphics('a=T,q=2,U=1,i=7,p=1,f=32,s=1,v=1,' check('virtual-pillarbox', graphics('a=T,q=2,U=1,i=7,p=1,f=32,s=1,v=1,'
'c=4,r=1', white_pixel) + colours + 'c=4,r=1', white_pixel) + colours +
ph + '\u0305\u0305' + ph + '\u0305\u030d', ph + '\u0305\u0305' + ph + '\u0305\u030d',
'2,0', render=(r'^(?:\x1b\[[0-9;]+m)* ' '2,0', render=(r'^(?:\x1b\[[0-9;]+m)*[ ▄]'
r'(?:\x1b\[[0-9;]+m)*\x1b\[48;2;255;255;255m ')) r'(?:\x1b\[[0-9;]+m)*\x1b\[48;2;255;255;255m[ ▄]'))
# Equal-z images follow source creation order, even with descending IDs. # Equal-z images follow source creation order, even with descending IDs.
older = graphics('a=t,q=2,i=200,f=32,s=1,v=1', pixel) older = graphics('a=t,q=2,i=200,f=32,s=1,v=1', pixel)
newer = graphics('a=t,q=2,i=100,f=32,s=1,v=1', white_pixel) newer = graphics('a=t,q=2,i=100,f=32,s=1,v=1', white_pixel)
put_older = graphics('a=p,q=2,C=1,i=200,c=1,r=1') put_older = graphics('a=p,q=2,C=1,i=200,c=1,r=1')
put_newer = graphics('a=p,q=2,C=1,i=100,c=1,r=1') put_newer = graphics('a=p,q=2,C=1,i=100,c=1,r=1')
check('same-z-descending-ids', older + newer + put_older + put_newer, check('same-z-descending-ids', older + newer + put_older + put_newer,
'0,0', render=r'48;2;255;255;255m ') '0,0', render=r'48;2;255;255;255m[ ▄]')
check('same-z-reversed-placements', check('same-z-reversed-placements',
older + newer + put_newer + put_older, older + newer + put_newer + put_older,
'0,0', render=r'48;2;255;255;255m ') '0,0', render=r'48;2;255;255;255m[ ▄]')
check('same-z-retransmit', check('same-z-retransmit',
older + newer + put_newer + older + put_older, older + newer + put_newer + older + put_older,
'0,0', render=r'48;2;255;255;255m ') '0,0', render=r'48;2;255;255;255m[ ▄]')
check('same-z-recreate', older + newer + put_newer + check('same-z-recreate', older + newer + put_newer +
graphics('a=d,d=I,q=2,i=200') + older + put_older, graphics('a=d,d=I,q=2,i=200') + older + put_older,
'0,0', render=r'48;2;255;0;0m ') '0,0', render=r'48;2;255;0;0m[ ▄]')
# An empty line tail must not discard the preceding background run. # An empty line tail must not discard the preceding background run.
background = '\033[11GX\033[17G\033[42m \033[0m' background = '\033[11GX\033[17G\033[42m \033[0m'
check('background-tail', background, '20,0', render=r'42m {4}') check('background-tail', background, '20,0', render=r'42m {4}')

View File

@@ -54,7 +54,7 @@ $TMUX new-session -d -s inner -x 40 -y 20 "printf '$HEADER'; exec sh" ||
exit 1 exit 1
sleep 0.3 sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 [ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 101
$TMUX set -as terminal-features ',*:kittygraphics' || exit 1 $TMUX set -as terminal-features ',*:kittygraphics' || exit 1
$TMUX2 new-session -d -x 40 -y 20 || exit 1 $TMUX2 new-session -d -x 40 -y 20 || exit 1

74
regress/image-lifecycle.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/sh
# Stress placement cleanup and copied references, restore image rows after
# height changes, and switch between graphical and fallback output.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import sys
from image_test import image_test, sixel, check_image
from image_terminal import Terminal
palette = [(100, 0, 0), (0, 100, 0)]
rgb = [(255, 0, 0), (0, 255, 0)]
wire = sixel(16, 32, palette,
[y >= 16 for y in range(32) for x in range(16)])
expected = [rgb[y >= 16] for y in range(32) for x in range(16)]
with image_test(sys.argv[1], sy=6) as test:
client = test.attach()
test.emit(wire)
test.resize(client, 40, 2)
test.redraw()
images = client.images()
assert len(images) == 1, [im[:2] for im in images]
check_image(images[0], 16, 16, [rgb[1]] * (16 * 16))
test.resize(client, 40, 6)
test.redraw()
assert len(client.images()) == 1
check_image(client.images()[0], 16, 32, expected)
# Changing pixel size can change the selected backend without reattaching.
with image_test(sys.argv[1]) as test:
client = test.attach()
test.emit(wire)
for _ in range(3):
test.resize(client, 40, 16, cw=0, ch=0)
test.redraw()
assert not client.images(), 'SIXEL backend retained without pixel size'
terminal = Terminal()
terminal.feed(client.data)
assert terminal.cells[0, 0][0] == '\u2584', terminal.cells[0, 0]
test.resize(client, 40, 16)
test.redraw()
assert len(client.images()) == 1, 'SIXEL backend not restored'
check_image(client.images()[0], 16, 32, expected)
# Keep one server alive through many overlapping placements, split spans,
# scrolling and clears, with copy-mode snapshots and resizes between batches.
with image_test(sys.argv[1]) as test:
client = test.attach()
overlay = sixel(16, 32, palette,
[None if x % 2 else 1 for y in range(32) for x in range(16)])
cycle = (b'\033[2J\033[H' + wire + b'\033[H' + overlay +
b'\033[1;2H\033[@\033[P\033[X\033[2;1H\033[L\033[M\033[2S')
for _ in range(10):
test.emit(cycle * 8 + b'\033[2J\033[H' + wire)
test.run('copy-mode')
test.run('send-keys', '-X', 'cancel')
test.resize(client, 1, 16)
test.resize(client, 40, 16)
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
terminal.check({(x, y): expected[y * 16 + x]
for y in range(32) for x in range(16)})
test.run('clear-history')
test.emit(b'\033[2J')
test.redraw()
assert not client.images(), 'images survived the final clear'
PY

View File

@@ -11,7 +11,7 @@ FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 30 -Y 5) ||
fail "new-pane -X -Y failed" fail "new-pane -X -Y failed"
sleep 0.3 sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX
$TMUX set -as terminal-features ',*:sixel' || exit 1 $TMUX set -as terminal-features ',*:sixel' || exit 1
start_capture 60 20 start_capture 60 20

View File

@@ -2,6 +2,8 @@
# keyboard interaction that must not retransmit an unrelated pane's image, # keyboard interaction that must not retransmit an unrelated pane's image,
# checked by counting DCS (\033P) sequences in the client's raw output. # checked by counting DCS (\033P) sequences in the client's raw output.
. ./image-common.inc
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 LC_ALL=C.UTF-8
@@ -64,6 +66,6 @@ assert_image_reached()
# see that test's header comment. # see that test's header comment.
assert_no_retransmit() assert_no_retransmit()
{ {
n=$(grep -ac "$(printf '\033P')" $TMP) n=$(grep -aoE "$(printf '\033P')[0-9;]*q" $TMP | wc -l)
[ "$n" -eq 0 ] || fail "image was retransmitted ($n times) $1" [ "$n" -eq 0 ] || fail "image was retransmitted ($n times) $1"
} }

View File

@@ -1,25 +1,9 @@
#!/bin/sh #!/bin/sh
# Verification test, not a known-bug regression test: checks whether a # Insert and delete a line behind a floating pane. The affected SIXEL
# structural screen operation (insert-line / delete-line) on a pane that is # image must be sent again even when the pane is obscured.
# currently obscured by a floating pane correctly retransmits an image
# sitting in the affected rows. . ./image-common.inc
#
# screen_write_insertline()/screen_write_deleteline() (screen-write.c), when
# the target pane is obscured, escalate via screen_write_redraw_pane() ->
# screen_write_redraw_line() -> tty_cmd_redrawline() -> tty_draw_line().
# This is a different, older mechanism from the sub-pane damage-rectangle
# system built elsewhere this session - it was never migrated onto it. But
# both functions also unconditionally call image_redraw_area() up front
# (independent of obscured status), and tty_draw_line() (tty-draw.c) has
# its own built-in ENABLE_IMAGES handling, so reading the code suggests
# this path should already correctly recomposite images, unlike the bugs
# fixed elsewhere this session where nothing signalled a redraw at all.
# This test exists to confirm that empirically rather than relying on that
# reading holding up in practice.
#
# If this ever fails, it means that reading was wrong and the obscured
# structural-op path needs migrating onto the damage system too.
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
@@ -47,29 +31,21 @@ cleanup
TMP=$(mktemp) TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15 trap "cleanup; rm -f $TMP" 0 1 15
# A small, distinctive SIXEL raster (26x26 pixels) at the top-left of the # Hold the pane after its image until capture is ready, then trigger the
# base pane, matching the fixture already used in image-support.sh. After a # insert/delete-line operation explicitly instead of racing a fixed sleep.
# pause (long enough for the floating pane below to be created), it emits a
# cursor-home, insert-line, delete-line sequence itself - a content no-op,
# but it exercises the obscured-pane structural-op redraw path across the
# image's rows. This is done from the pane's own script rather than typed
# via send-keys, since interactive typing races against the shell's own
# line-editing redraws and does not reliably reach the terminal parser as
# real escape sequences.
SIXEL='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\' SIXEL='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
TRIGGER='\033[H\033[L\033[M' TRIGGER='\033[H\033[L\033[M'
$TMUX new-session -d -s inner -x 40 -y 15 \ $TMUX new-session -d -s inner -x 40 -y 15 \
"printf '$SIXEL'; sleep 2; printf '$TRIGGER'; sleep 100" || exit 1 "printf '$SIXEL'; $TMUX wait-for image-ops-$$; printf '$TRIGGER'; sleep 100" || exit 1
sleep 0.5 sleep 0.5
BASE=$($TMUX list-panes -F '#{pane_id}' | head -1) BASE=$($TMUX list-panes -F '#{pane_id}' | head -1)
[ -n "$BASE" ] || fail "No base pane." [ -n "$BASE" ] || fail "No base pane."
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX
$TMUX set -as terminal-features ',*:sixel' || exit 1 $TMUX set -as terminal-features ',*:sixel' || exit 1
# Float a small pane so it overlaps the top rows of the base pane, where # Float a small pane so it overlaps the top rows of the base pane, where
# the image sits - this is what makes the base pane obscured there. This # the image sits, making the base pane obscured.
# must happen before the base pane's 2-second internal sleep elapses.
$TMUX new-pane -d -x 12 -y 4 -X 5 -Y 0 || fail "new-pane -X -Y failed" $TMUX new-pane -d -x 12 -y 4 -X 5 -Y 0 || fail "new-pane -X -Y failed"
$TMUX2 new-session -d -x 40 -y 15 "$TMUX attach -t inner" || exit 1 $TMUX2 new-session -d -x 40 -y 15 "$TMUX attach -t inner" || exit 1
@@ -78,7 +54,12 @@ OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane." [ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed" $TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
sleep 2.5 $TMUX refresh-client || fail "initial refresh failed"
sleep 1
grep -aq '"1;1;26;26' $TMP || fail "initial image was not sent"
: >$TMP
$TMUX wait-for -S image-ops-$$ || fail "trigger failed"
sleep 1
grep -a -q '"1;1;26;26' $TMP || grep -a -q '"1;1;26;26' $TMP ||
fail "image was not retransmitted after insert/delete-line on an obscured pane" fail "image was not retransmitted after insert/delete-line on an obscured pane"

View File

@@ -20,6 +20,7 @@ $TMUX2 new-session -d -x 10 -y 4 "
i=\$((i + 1)) i=\$((i + 1))
done done
sleep 30" || exit 1 sleep 30" || exit 1
[ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 101
$TMUX2 set -g status off || exit 1 $TMUX2 set -g status off || exit 1
$TMUX new-session -d -x 10 -y 4 "$TMUX2 attach-session" || exit 1 $TMUX new-session -d -x 10 -y 4 "$TMUX2 attach-session" || exit 1
$TMUX set -g status off || exit 1 $TMUX set -g status off || exit 1

View File

@@ -1,39 +1,49 @@
#!/bin/sh #!/bin/sh
# Widening a pane restores clipped columns for every image placement. # Adapt the PR's many-placement recovery test to SIXEL: place images on
# separate rows so they remain live, then recover every clipped column.
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C export TERM
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Limage-resize-outer$$ -f/dev/null"
TMUX2="env -u TMUX LC_ALL=C $TEST_TMUX -Limage-resize-inner$$ -f/dev/null"
TMP=$(mktemp)
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15
# White placements stay behind the final red placement. python3 -B - "$TEST_TMUX" <<'PY'
# An 8:1 source fills 16 columns by one row without letterboxing. import sys
$TMUX2 new-session -d -x 8 -y 4 " from image_test import image_test, sixel, check_image
i=0
while [ \$i -lt 64 ]; do
printf '\\033_Ga=T,q=2,C=1,f=32,s=8,v=1,c=16,r=1;//////////////////////////////////////////8=\\033\\\\'
i=\$((i + 1))
done
printf '\\033_Ga=T,q=2,C=1,f=32,s=8,v=1,c=16,r=1;/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP8=\\033\\\\'
sleep 30" || exit 1
[ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX2 set -g status off || exit 1
$TMUX new-session -d -x 8 -y 4 "$TMUX2 attach-session" || exit 1
$TMUX set -g status off || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1 def check_rows(images, width):
[ "$(cat $TMP)" = '........' ] || exit 1 """Each placement has two coloured halves; rows may use several rasters."""
$TMUX resize-window -x 16 -y 4 || exit 1 row = x = 0
sleep 1 for image in images:
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1 assert row < 64 and x + image[0] <= width, (row, x, image[:2])
[ "$(cat $TMP)" = '................' ] || exit 1 expected = [rgb[(row + (xx >= 64)) % 3]
for xx in range(x, x + image[0])]
check_image(image, image[0], 1, expected)
x += image[0]
if x == width:
row += 1
x = 0
assert (row, x) == (64, 0), (row, x)
exit 0
palette = [(100, 0, 0), (0, 100, 0), (0, 0, 100)]
rgb = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
with image_test(sys.argv[1], sx=8, sy=80) as test:
client = test.attach()
wire = b''
for y in range(64):
wire += ('\033[%d;1H' % (y + 1)).encode()
wire += sixel(128, 1, palette,
[y % 3] * 64 + [(y + 1) % 3] * 64)
test.emit(wire)
test.redraw()
check_rows(client.images(), 64)
test.resize(client, 16, 80)
test.redraw()
check_rows(client.images(), 128)
test.redraw()
check_rows(client.images(), 128)
PY

View File

@@ -1,89 +1,46 @@
#!/bin/sh #!/bin/sh
# Widening a pane must recover image columns clipped at placement time. # The PR's SIXEL fixture must recover all columns clipped at placement time.
# A damage redraw can send the newly visible columns as a separate raster, # A PTY gives a known cell size; allow restored rows to use several rasters.
# so compare the combined raster widths with the original image width.
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 export TERM
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup() python3 -B - "$TEST_TMUX" <<'PY'
{ from pathlib import Path
$TMUX kill-server >/dev/null 2>&1 import re
$TMUX2 kill-server >/dev/null 2>&1 import sys
} from image_test import image_test
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup wire = Path('monkey-2.sixel.txt').read_bytes()
match = re.search(rb'"1;1;(\d+);(\d+)', wire)
assert match is not None
w, h = map(int, match.groups())
TMP=$(mktemp) with image_test(sys.argv[1], sx=12, sy=40) as test:
trap "cleanup; rm -f $TMP" 0 1 15 client = test.attach()
test.emit(wire)
test.redraw()
images = client.images()
assert len(images) == 1, [im[:2] for im in images]
assert images[0][:2] == (96, h), images[0][:2]
FIXTURE=$(pwd)/monkey-2.sixel.txt test.resize(client, 80, 40)
FULL=$(grep -oa '"1;1;[0-9]*;[0-9]*' $FIXTURE | head -1 | cut -d';' -f3) test.redraw()
[ -n "$FULL" ] || fail "could not read fixture's own raster width" images = client.images()
assert images, 'no image after widening'
# The fixture is 360px wide - narrow enough that a 12-column pane clips it, x = y = 0
# comfortably below the 360px it would take to show in full. row_h = None
$TMUX new-session -d -s inner -x 12 -y 40 "cat '$FIXTURE'; exec sh" || exit 1 for im in images:
sleep 0.5 if x == 0:
row_h = im[1]
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 assert im[1] == row_h, (im[:2], row_h)
$TMUX set -as terminal-features ',*:sixel' || exit 1 x += im[0]
assert x <= w, (x, w)
# Start the outer session with a plain shell, then start capturing before if x == w:
# triggering the attach - starting the attach as the outer pane's initial y += row_h
# command would mean pipe-pane only starts after the attach-driven initial x = 0
# redraw (which sends the image) has already happened, missing it. assert (x, y) == (0, h), ('image stayed clipped', x, y, w, h)
$TMUX2 new-session -d -x 12 -y 40 || exit 1 PY
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# The outer client is just another tmux client receiving raw PTY output
# that happens to contain sixel DCS sequences - left alone, it would
# independently decode and place its own second, differently-sized image
# on top of the inner session's, confounding the raster-width measurement
# below. Disable sixel on the outer client so only the inner session's
# placement exists.
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
before=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -n | tail -1)
[ -n "$before" ] || fail "sanity: image never reached the client"
[ "$before" -lt 360 ] || fail "sanity: image was not clipped by the narrow pane ($before)"
: >$TMP
# Widen the window well past the image's full width - resize the outer
# client, since the inner session's displayed size follows whatever
# terminal size its attached client actually has.
$TMUX2 resize-window -t "$OUTER" -x 30 -y 40 || fail "resize-window failed"
sleep 1
$TMUX refresh-client || fail "refresh-client failed"
sleep 1
# The widths seen after the resize, added together (deduplicated - the
# same chunk may legitimately be retransmitted), should account for the
# fixture's full pixel width: this is expected to fail before the fix,
# where the newly-uncovered columns are never redrawn at all and only the
# original clipped width ever appears - see the header comment.
after_total=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -nu |
awk '{s+=$1} END{print s+0}')
[ "$after_total" -gt 0 ] ||
fail "no image raster was sent at all after widening the window"
[ "$after_total" -ge $((FULL - 20)) ] ||
fail "image stayed clipped at ${before}px after widening the window (widths summed to only ${after_total}px, expected close to ${FULL}px)"
exit 0

View File

@@ -1,107 +1,16 @@
#!/bin/sh #!/bin/sh
# Regression test for a tmux bug (not a terminal bug): image_redraw_scroll() # The PR's trusted-scroll erase regression: inspect terminal erases directly
# (image.c) marks the whole scroll region as damage on every scroll. For a # rather than depending on a particular debug log message.
# client whose terminal has imagescroll, the terminal has already moved the
# image (and the text) along with the scroll, but the damage was still
# composed: the image was not retransmitted, but the ordinary text pass
# drew blank grid content directly over the image's cells and erased a
# correctly scrolled image. This is what a user saw as "the image
# disappears when the pane scrolls in Windows Terminal" even with
# imagescroll and margins both granted, and could never be reproduced by
# any terminal-side test because the bug is entirely server side.
#
# redraw_client_damage_rect() (screen-redraw.c) now composes nothing for a
# scroll the client's terminal is trusted to have done itself, logging
# "composing damage" only when it draws. Nothing being drawn is what keeps
# the image, so the absence of that log line for the scroll is checked.
#
# The image is placed comfortably mid-pane (not at the very top) and wide
# enough to span the whole pane, so it is still on screen - not already
# scrolled into history - by the time a real scroll's damage gets composed.
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 export TERM
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -vv -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup() python3 -B - "$TEST_TMUX" <<'PY'
{ import sys
$TMUX kill-server >/dev/null 2>&1 from image_scroll import check_scroll
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup check_scroll(sys.argv[1], True, no_erase=True)
PY
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
trap "cleanup; cd /; rm -rf $DIR" 0 1 15
WIDTH=40
HEIGHT=20
ROW=10
# A sixel image exactly WIDTH cells wide (at a typical ~9px cell, 360px) and
# one cell tall, placed at screen row ROW - comfortably mid-pane, not at
# the very top, so it stays visible (not yet scrolled into history) through
# the first several scrolls.
HEADER='\033Pq"1;1;360;18#0;2;100;100;100#0!360~-!360~-!360B\033\\'
# Pad with blank lines after the image so the shell prompt lands at the
# bottom of the pane - without this, the very first scroll from a
# not-yet-settled cursor position falls back to tty_redraw_region() (a
# different, unrelated path that the trusted-scroll skip never applies to
# anyway), and the test would not exercise what it is meant to.
PAD=$((HEIGHT - ROW - 2))
$TMUX new-session -d -s inner -x $WIDTH -y $HEIGHT \
"printf '\\033[$((ROW + 1));1H$HEADER'; i=0; while [ \$i -lt $PAD ]; do echo; i=\$((i + 1)); done; exec sh" ||
exit 1
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -g pane-scrollbars on || exit 1
$TMUX set -as terminal-features ',*:sixel' || exit 1
$TMUX set -as terminal-features ',*:margins' || exit 1
$TMUX set -as terminal-features ',*:imagescroll' || exit 1
$TMUX2 new-session -d -x $WIDTH -y $HEIGHT || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
TMP=$(mktemp)
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
grep -qa '"1;1;360;18' "$TMP" || fail "sanity: image never reached the client"
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
BEFORE=$(wc -l <"$LOG")
: >"$TMP"
# The image (row ROW, 10 of 20) has plenty of room above it - this stays
# well within the visible pane for the whole burst.
$TMUX send-keys -t inner -l "yes | head -n 10" || fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
n_dcs=$(grep -ac "$(printf '\033P')" "$TMP")
[ "$n_dcs" -eq 0 ] ||
fail "sanity: image was retransmitted ($n_dcs times) - imagescroll's trust did not engage, so this run cannot test what it is meant to"
tail -n +$((BEFORE + 1)) "$LOG" | grep -q "redraw_client_damage_rect: .* composing damage" &&
fail "damage was composed for a scroll the terminal is trusted to have done - this would draw over the image and erase it"
exit 0

View File

@@ -1,87 +1,17 @@
#!/bin/sh #!/bin/sh
# When the client's terminal has the imagescroll feature, tmux trusts it to # Adapt the PR's Kitty marker check to SIXEL. Native image scrolling must
# have moved everything in the scrolled region - text and image alike - so # skip the surrounding text repaint as well as image retransmission.
# a scroll must not cause any existing text to be redrawn. It used to skip
# only the image phases of the damage redraw and still redraw all of the
# text around them.
#
# Without imagescroll the images must be redrawn, and the text around them
# is redrawn too, so the same scroll must resend it.
#
# The feature only takes effect for a client attached after it is set, so
# each case starts a new server and client.
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 export TERM
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup() python3 -B - "$TEST_TMUX" <<'PY'
{ import sys
$TMUX kill-server >/dev/null 2>&1 from image_scroll import check_scroll
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
TMP=$(mktemp) check_scroll(sys.argv[1], True)
trap "cleanup; rm -f $TMP" 0 1 15 check_scroll(sys.argv[1], False)
PY
HEADER=$(printf '\033_Ga=T,q=2,f=32,s=1,v=1,i=424243;/wAA/w==\033\\')
# Scroll a pane containing an image and marker text on a client with the
# given terminal features, and set N to the number of marker lines sent to
# the terminal.
scroll_markers()
{
cleanup
# Marker text around an image, filling the pane so that the next
# newlines scroll it while the image stays in view.
$TMUX new-session -d -s inner -x 40 -y 20 \
"i=0; while [ \$i -lt 5 ]; do printf 'MARKER%02d\n' \$i; i=\$((i + 1)); done; printf '$HEADER'; while [ \$i -lt 17 ]; do printf 'MARKER%02d\n' \$i; i=\$((i + 1)); done; exec sh" ||
exit 1
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features "$1" || exit 1
$TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:kittygraphics@' ||
fail "disable outer kitty failed"
: >"$TMP"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" ||
fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 2
grep -qa "$(printf '\033_Ga=p')" "$TMP" ||
fail "sanity: image never reached the client"
: >"$TMP"
$TMUX send-keys -t inner -l "printf '\n\n\n'" ||
fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
N=$(grep -ac MARKER "$TMP")
}
scroll_markers ',*:kittygraphics,*:imagescroll'
[ "$N" -eq 0 ] ||
fail "existing text was redrawn ($N lines) scrolling with imagescroll"
scroll_markers ',*:kittygraphics'
[ "$N" -gt 0 ] ||
fail "text was not redrawn scrolling without imagescroll"
exit 0

View File

@@ -2,6 +2,8 @@
# Images scrolled off-screen must reappear when copy mode scrolls back up. # Images scrolled off-screen must reappear when copy mode scrolls back up.
. ./image-common.inc
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 LC_ALL=C.UTF-8
@@ -37,7 +39,7 @@ FIXTURE=$(pwd)/monkey-2.sixel.txt
$TMUX new-session -d -s inner -x 30 -y 5 "cat '$FIXTURE'; exec sh" || exit 1 $TMUX new-session -d -s inner -x 30 -y 5 "cat '$FIXTURE'; exec sh" || exit 1
sleep 0.5 sleep 0.5
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX
$TMUX set -as terminal-features ',*:sixel' || exit 1 $TMUX set -as terminal-features ',*:sixel' || exit 1
# Start the outer session with a plain shell, then start capturing before # Start the outer session with a plain shell, then start capturing before

View File

@@ -1,142 +1,20 @@
#!/bin/sh #!/bin/sh
# Regression test: with imagescroll granted, does tmux actually use a native # The PR's narrow-pane scroll check. Margins permit native scrolling;
# scrolling-region command (DECSTBM, and DECSLRM if the pane isn't full # without them imagescroll must not suppress a necessary repaint.
# width) to perform the scroll, rather than tty_redraw_region()'s fallback # Different clients must make that decision independently.
# full manual repaint? This directly checks the concern raised in PR review
# that tmux "isn't using scrolling regions" for an image-bearing pane -
# imagescroll only makes sense to grant a terminal if tmux is actually
# relying on a real native scroll to carry the image along, not silently
# falling back to redrawing every line by hand while still skipping the
# image retransmission (which would just lose the image for nothing).
#
# tty_redraw_region() (tty.c) logs one of two lines whenever it runs at
# all - "%s large region redraw" or "%s small region redraw" - so their
# total absence after the scroll proves the native path
# (tty_cmd_linefeed()/scrollup()/scrolldown()/reverseindex()) was taken
# instead. Combined with image-sixel-region-scroll.sh's own check (the
# image is not retransmitted), this proves imagescroll's trust is actually
# backed by a real native scroll, not a coincidence.
#
# Phase 2 is the deliberate opposite case, and is what PR review's concern
# actually described: pane-scrollbars makes this pane less than full width,
# and without margins granted, tty_cmd_*()'s own
# "(!tty_full_width(tty, ctx) && !tty_use_margin(tty))" check has no native
# scrolling-region option left and must fall back - proving phase 1 wasn't
# trivially passing, and that redraw_image_scroll_result()'s failure path
# correctly also stops imagescroll's trust from applying when that happens
# (the image must be retransmitted here, exactly as
# image-sixel-region-scroll.sh's own phase 2 already separately proves).
#
# A real attached client is needed for any of this redraw code to run at
# all, so this uses the same nested inner/outer tmux pattern as
# image-sixel-region-scroll.sh.
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 export TERM
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -vv -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup() python3 -B - "$TEST_TMUX" <<'PY'
{ import sys
$TMUX kill-server >/dev/null 2>&1 from image_scroll import check_scroll, check_clients
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup check_scroll(sys.argv[1], True, margins=True)
check_scroll(sys.argv[1], True, margins=False)
DIR=$(mktemp -d) || exit 1 check_clients(sys.argv[1])
cd "$DIR" || exit 1 check_clients(sys.argv[1], narrow=True)
trap "cleanup; cd /; rm -rf $DIR" 0 1 15 PY
WIDTH=40
HEIGHT=20
HEADER='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
$TMUX new-session -d -s inner -x $WIDTH -y $HEIGHT \
"printf '$HEADER'; exec sh" || exit 1
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -g pane-scrollbars on || exit 1
$TMUX set -as terminal-features ',*:sixel' || exit 1
$TMUX set -as terminal-features ',*:imagescroll' || exit 1
$TMUX2 new-session -d -x $WIDTH -y $HEIGHT || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
TMP=$(mktemp)
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image never reached the client"
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
run_scroll()
{
label=$1
BEFORE=$(wc -l <"$LOG")
: >"$TMP"
$TMUX send-keys -t inner -l "yes | head -n 10" || fail "$label: send scroll failed"
$TMUX send-keys -t inner Enter || fail "$label: send enter failed"
sleep 1
n_dcs=$(grep -ac "$(printf '\033P')" "$TMP")
n_redraw=$(tail -n +$((BEFORE + 1)) "$LOG" | grep -c "region redraw")
}
# --- Phase 1: margins granted - real native scrolling region expected,
# image never retransmitted. ---
$TMUX set -as terminal-features ',*:margins' || fail "grant margins failed"
sleep 0.3
$TMUX display-message -p '#{client_termfeatures}' |
grep -q imagescroll || fail "sanity: imagescroll was not granted to the client"
run_scroll "phase 1 (margins granted)"
[ "$n_dcs" -eq 0 ] ||
fail "phase 1: image was retransmitted ($n_dcs times) - imagescroll's trust did not engage, so this run cannot test what it is meant to"
[ "$n_redraw" -eq 0 ] ||
fail "phase 1: tty_redraw_region() fired ($n_redraw times) while scrolling an image-bearing pane with margins+imagescroll granted - tmux fell back to a full manual repaint instead of a real native scroll, so trusting the terminal to have moved the image was never justified"
# --- Phase 2: margins revoked - the pane still isn't full width
# (pane-scrollbars is on), so this is the scenario PR review's "not using
# scrolling regions" concern actually described: no native scrolling-region
# option is available, tty_redraw_region() must fire, and
# redraw_image_scroll_result()'s failure path must in turn stop
# imagescroll's trust from applying, so the image is retransmitted instead
# of silently lost. Resend the image first - phase 1's scrolling already
# pushed the original one (26 pixels tall, a couple of rows at most) out of
# the pane, so without this there would be nothing left to retransmit
# either way, and the check below would pass for the wrong reason. ---
$TMUX set -as terminal-features ',*:margins@' || fail "revoke margins failed"
sleep 0.3
: >"$TMP"
$TMUX send-keys -t inner -l "printf '$HEADER'" || fail "resend image failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1.5
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image did not reappear before phase 2"
run_scroll "phase 2 (margins revoked)"
[ "$n_redraw" -gt 0 ] ||
fail "sanity: without margins, scrolling a scrollbar-enabled image-bearing pane never fell back to tty_redraw_region() - this scenario no longer exercises the bug phase 1 checks for"
[ "$n_dcs" -gt 0 ] ||
fail "phase 2: image was not retransmitted after tty_redraw_region() fired - imagescroll's trust incorrectly stayed engaged despite the fallback, which would silently lose the image"
exit 0

View File

@@ -12,7 +12,7 @@ $TMUX new-session -d -s inner -x 40 -y 20 \
exit 1 exit 1
sleep 0.5 sleep 0.5
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX
$TMUX set -as terminal-features ',*:sixel' || exit 1 $TMUX set -as terminal-features ',*:sixel' || exit 1
start_capture 40 20 start_capture 40 20

96
regress/image-sixel-output.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/bin/sh
# Check RGBA to SIXEL output, palette splitting, transparent edges, source
# clipping and repeated redraws at different terminal pixel sizes.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import sys
from image_test import image_test, sixel, check_image
palette = [(100, 0, 0), (0, 100, 0), (0, 0, 100), (100, 100, 100)]
rgb = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)]
pixels = [2 * (y >= 16) + (x >= 8) for y in range(32) for x in range(16)]
expected = [rgb[c] for c in pixels]
wire = sixel(16, 32, palette, pixels)
with image_test(sys.argv[1]) as test:
client = test.attach()
test.emit(wire)
assert len(client.images()) == 1
check_image(client.images()[0], 16, 32, expected)
for _ in range(3):
test.redraw()
assert len(client.images()) == 1
check_image(client.images()[0], 16, 32, expected)
# Keep the stored cell footprint when terminal cell pixels change.
for cw, ch in [(16, 32), (12, 24), (12, 16)]:
test.resize(client, 40, 16, cw=cw, ch=ch)
w, h = cw * 2, ch * 2
scaled = [expected[(y * 32 // h) * 16 + x * 16 // w]
for y in range(h) for x in range(w)]
assert len(client.images()) == 1
check_image(client.images()[0], w, h, scaled)
test.resize(client, 40, 16)
check_image(client.images()[0], 16, 32, expected)
# Removing one grid cell crops the original source, retaining its colours.
test.emit(b'\033[HX')
images = client.images()
assert len(images) == 2, [im[:2] for im in images]
check_image(images[0], 8, 16, [rgb[1]] * (8 * 16))
check_image(images[1], 16, 16,
[rgb[2 + (x >= 8)] for y in range(16) for x in range(16)])
test.emit(b'\033[2J\033[H')
assert not client.images()
test.redraw()
assert not client.images()
# A partial last cell keeps its actual pixel size instead of stretching.
test.emit(sixel(26, 26, [(100, 100, 100)], [0] * (26 * 26)))
assert len(client.images()) == 1
check_image(client.images()[0], 26, 26, [rgb[3]] * (26 * 26))
# Transparent edges and holes must not acquire coloured pixels.
pixels = [None if x == 0 or y == 0 or x == 7 or y == 15 else 0
for y in range(16) for x in range(8)]
test.emit(b'\033[2J\033[H' + sixel(8, 16, palette[:1], pixels))
assert len(client.images()) == 1
check_image(client.images()[0], 8, 16,
[None if c is None else rgb[0] for c in pixels])
# A skewed histogram still splits into black, grey and white entries.
pixels = [0] * 10 + [1] * 10 + [2] * 80
test.emit(b'\033[2J\033[H' +
sixel(100, 1, [(0, 0, 0), (50, 50, 50), (100, 100, 100)], pixels))
assert len(client.images()) == 1
check_image(client.images()[0], 100, 1,
[(0, 0, 0)] * 10 + [(128, 128, 128)] * 10 + [rgb[3]] * 80)
# The right edge of a pane crops the source, rather than rescaling it.
test.emit(b'\033[2J\033[1;40H' + wire)
assert len(client.images()) == 1
check_image(client.images()[0], 8, 32,
[rgb[2 * (y >= 16)] for y in range(32) for x in range(8)])
# Different clients independently use the same image at their own cell size.
with image_test(sys.argv[1]) as test:
large = test.attach()
test.emit(wire)
small = test.attach(cw=4, ch=8)
fallback = test.attach(graphical=False)
test.redraw()
assert len(large.images()) == 1
check_image(large.images()[0], 16, 32, expected)
assert len(small.images()) == 1
check_image(small.images()[0], 8, 16,
[expected[y * 2 * 16 + x * 2]
for y in range(16) for x in range(8)])
assert not fallback.images()
assert b'\xe2\x96\x84' in fallback.data, 'missing simultaneous fallback'
PY

View File

@@ -10,6 +10,8 @@
# covered must survive, so a second case draws a narrower image over a wider # covered must survive, so a second case draws a narrower image over a wider
# one and expects both. # one and expects both.
. ./image-common.inc
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 LC_ALL=C.UTF-8
@@ -43,7 +45,7 @@ count_images()
$TMUX new-session -d -s inner -x 40 -y 10 "printf '$1'; exec sh" || $TMUX new-session -d -s inner -x 40 -y 10 "printf '$1'; exec sh" ||
exit 1 exit 1
sleep 0.3 sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX
$TMUX set -as terminal-features ',*:sixel' || exit 1 $TMUX set -as terminal-features ',*:sixel' || exit 1
$TMUX2 new-session -d -x 40 -y 10 || exit 1 $TMUX2 new-session -d -x 40 -y 10 || exit 1

45
regress/image-sixel-palette.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/sh
# Reduce a 512-colour cube to a SIXEL palette. Check colour error, opaque
# pixels, palette size, extremes and reproducible output after redraws.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import base64
import sys
from image_test import image_test, sixel
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]
expected = [tuple((c * 255 + 50) // 100 for c in p) for p in palette]
with image_test(sys.argv[1]) as test:
client = test.attach()
test.emit(sixel(32, 16, palette, list(range(512))))
images = client.images()
assert len(images) == 1 and images[0][:2] == (32, 16)
assert images[0][2] == expected, 'original SIXEL palette changed'
test.redraw()
assert client.images() == images, 'retained palette changed on redraw'
# Kitty input exercises RGBA quantization rather than SIXEL retention.
rgba = bytes(c for pixel in expected for c in (*pixel, 255))
test.emit(b'\033[2J\033[H\033_Ga=T,q=2,f=32,s=32,v=16;' +
base64.b64encode(rgba) + b'\033\\')
images = client.images()
assert len(images) == 1 and images[0][:2] == (32, 16)
actual = images[0][2]
assert all(c is not None for c in actual), 'dithering introduced holes'
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)]
assert max(errors) <= 80, max(errors)
assert sum(errors) / len(errors) <= 16, sum(errors) / len(errors)
assert max(actual[0]) <= 30 and min(actual[-1]) >= 225, (
'black/white extremes lost', actual[0], actual[-1])
for _ in range(3):
test.redraw()
assert client.images() == images, 'palette or dithering changed on redraw'
PY

68
regress/image-sixel-parse.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/bin/sh
# Valid, fragmented and rejected SIXEL input, including the PR's stray
# character case and limits reached while growing the image.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import sys
from image_test import image_test, sixel, check_image
with image_test(sys.argv[1]) as test:
client = test.attach()
red = (255, 0, 0)
valid = sixel(8, 16, [(100, 0, 0)], [0] * (8 * 16))
# Check a clean image and the same one with stray bytes between bands.
for wire in (valid, valid.replace(b'-', b' \t\177\200-', 1)):
test.emit(b'\033[2J\033[H' + wire)
images = client.images()
assert len(images) == 1, len(images)
check_image(images[0], 8, 16, [red] * (8 * 16))
# HLS uses blue at 0, red at 120 and green at 240 degrees.
for hue, rgb in [(0, (0, 0, 255)), (120, red), (240, (0, 255, 0))]:
wire = ('\033P9;1q"1;1;8;16#0;1;%d;50;100'
'#0!8~-!8~-!8N\033\\' % hue).encode()
test.emit(b'\033[2J\033[H' + wire)
assert len(client.images()) == 1
check_image(client.images()[0], 8, 16, [rgb] * (8 * 16))
# Undefined registers are black in the shared model and in retained output.
test.emit(b'\033[2J\033[H\033P9;1q"1;1;8;16'
b'#5!8~-!8~-!8N\033\\')
assert len(client.images()) == 1
check_image(client.images()[0], 8, 16, [(0, 0, 0)] * (8 * 16))
test.redraw()
check_image(client.images()[0], 8, 16, [(0, 0, 0)] * (8 * 16))
# Deliver the terminator in two pieces. No image is placed before it.
test.emit(b'\033[2J\033[H')
for byte in valid[:-1]:
test.send(bytes([byte]))
client.read()
assert not client.images(), 'image placed before the DCS terminator'
assert test.run('display-message', '-p', '#{cursor_y}') == '0'
test.emit(valid[-1:])
assert len(client.images()) == 1
check_image(client.images()[0], 8, 16, [red] * (8 * 16))
rejected = [
b'q', b'q#0;2;100', b'q!0~', b'q!10001~', b'q#1025~',
b'q#0;2;101;0;0~', b'q#0;1;361;50;100~',
b'q"1;1;10001;1', b'q"1;1;1;10001',
b'q"1;1;4096;4097',
b'q"1;1;18446744073709551616;1',
b'q"1;1;10000;1#0;2;100;0;0@' + b'-' * 300 + b'@',
b'q"1;1;1;10000#0;2;100;0;0!2000@',
]
for body in rejected:
test.emit(b'\033[2J\033[H\033P9;1' + body + b'\033\\')
assert not client.images(), ('invalid image accepted', body[:80])
assert test.run('display-message', '-p', '#{cursor_x},#{cursor_y}') == '0,0'
test.redraw()
assert not client.images(), ('invalid image retained', body[:80])
PY

View File

@@ -1,96 +1,21 @@
#!/bin/sh #!/bin/sh
# Regression test for the imagescroll terminal-feature (tty-features.c), # The PR's SIXEL imagescroll check, with known cell pixels and no shell race.
# SIXEL side - see image-kitty-region-scroll.sh for the Kitty side of the # Check the default repaint and an overwrite merged with trusted scroll damage.
# same feature: scrolling a pane that has a SIXEL image in it must not
# retransmit the image when the client's terminal has imagescroll - tmux
# trusts the terminal to have moved the image along with the rest of the
# scrolling region. Without imagescroll (the default for any terminal not
# individually confirmed and added to tty-features.c's table), today's
# always-redraw-on-scroll behaviour is unchanged.
#
# There is no way to query a terminal for whether it actually moves SIXEL
# pixels along with a scroll, so this only proves tmux's own decision to
# skip or redraw is wired correctly - not that any particular terminal
# renders the result correctly. That needs a human, on real terminals.
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
LC_ALL=C.UTF-8 export TERM
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup() python3 -B - "$TEST_TMUX" <<'PY'
{ import sys
$TMUX kill-server >/dev/null 2>&1 from image_scroll import check_scroll, check_capability
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup for command in ('linefeed', 'up', 'down', 'reverse'):
check_scroll(sys.argv[1], True, command=command)
TMP=$(mktemp) check_scroll(sys.argv[1], False, command=command)
trap "cleanup; rm -f $TMP" 0 1 15 check_scroll(sys.argv[1], True, mixed=True)
check_capability(sys.argv[1], True)
HEADER='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\' check_capability(sys.argv[1], False)
PY
# A small image near the top of a tall pane, then enough plain output below
# it to force several ordinary (no DECSTBM sub-region) linefeed scrolls,
# each one shifting the image rows along with everything else.
$TMUX new-session -d -s inner -x 40 -y 20 "printf '$HEADER'; exec sh" ||
exit 1
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:sixel' || exit 1
$TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 2
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image never reached the client"
# --- Phase 1: imagescroll granted - expect no DCS. ---
$TMUX set -as terminal-features ',*:imagescroll' || fail "grant imagescroll failed"
sleep 0.5
: >"$TMP"
$TMUX send-keys -t inner -l "yes | head -n 30" || fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
n_on=$(grep -ac "$(printf '\033P')" "$TMP")
[ "$n_on" -eq 0 ] ||
fail "image was retransmitted ($n_on times) scrolling with imagescroll granted"
# --- Phase 2: imagescroll not granted (the default) - expect the image
# back, then more scrolling to redraw it. ---
$TMUX send-keys -t inner -l "printf '$HEADER'" || fail "resend image failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1.5
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image did not reappear before phase 2"
$TMUX set -as terminal-features ',*:imagescroll@' || fail "revoke imagescroll failed"
sleep 0.5
: >"$TMP"
$TMUX send-keys -t inner -l "yes | head -n 30" || fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
n_off=$(grep -ac "$(printf '\033P')" "$TMP")
[ "$n_off" -gt 0 ] ||
fail "image was not retransmitted scrolling without imagescroll - expected the default always-redraw behaviour"
exit 0

View File

@@ -0,0 +1,54 @@
#!/bin/sh
# Transparent overlap preserves underlying pixels; opaque background modes
# replace them. Check positions, redraws, resize and erasing stacked images.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 -B - "$TEST_TMUX" <<'PY'
import sys
from image_test import image_test, sixel
from image_terminal import Terminal
w, h = 24, 32
holes = [None if x // 8 == 1 and y < 16 or (x + y) % 3 == 0 else 0
for y in range(h) for x in range(w)]
base = sixel(w, h, [(100, 0, 0)], [0] * (w * h))
for p2 in (0, 1, 2):
with image_test(sys.argv[1]) as test:
client = test.attach()
test.emit(b'\033[3;3H' + base + b'\033[3;3H' +
sixel(w, h, [(0, 0, 100)], holes, p2=p2))
background = (255, 0, 0) if p2 == 1 else (0, 0, 0)
expected = {(x + 16, y + 32): (0, 0, 255)
if holes[y * w + x] == 0 else background
for y in range(h) for x in range(w)}
for width in (40, 4, 40):
test.resize(client, width, 16)
test.redraw()
terminal = Terminal(sx=width)
terminal.feed(client.data)
terminal.check({point: c for point, c in expected.items()
if point[0] < width * 8})
# One erase must remove the same cell from every overlapping placement.
test.emit(b'\033[3;3H\033[X')
terminal.feed(client.data)
expected = {point: c for point, c in expected.items()
if not (16 <= point[0] < 24 and 32 <= point[1] < 48)}
terminal.check(expected)
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
terminal.check(expected)
with image_test(sys.argv[1]) as test:
client = test.attach()
test.emit(sixel(24, 32, [(100, 0, 0)], [None] * (24 * 32)))
assert not client.images(), 'fully transparent image produced output'
test.redraw()
assert not client.images(), 'fully transparent image survived redraw'
PY

View File

@@ -10,7 +10,7 @@ $TMUX new-session -d -s inner -x 60 -y 20 "printf '$SIXEL_HEADER'; exec sh" ||
$TMUX set -g mouse on || fail "set mouse failed" $TMUX set -g mouse on || fail "set mouse failed"
sleep 0.3 sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX
$TMUX set -as terminal-features ',*:sixel' || exit 1 $TMUX set -as terminal-features ',*:sixel' || exit 1
start_capture 60 20 start_capture 60 20

View File

@@ -34,7 +34,7 @@ $TMUX split-window -h -t inner 'exec sh' || fail "split-window failed"
$TMUX select-pane -t inner.0 || fail "select-pane failed" $TMUX select-pane -t inner.0 || fail "select-pane failed"
sleep 0.3 sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 image_check_support $TMUX
$TMUX set -as terminal-features ',*:sixel' || exit 1 $TMUX set -as terminal-features ',*:sixel' || exit 1
start_capture 60 12 start_capture 60 12

View File

@@ -25,7 +25,7 @@ $TMUX new-session -d -x 20 -y 8 "
sleep 10" sleep 10"
sleep 1 sleep 1
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 [ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 101
[ "$($TMUX display-message -p '#{sixel_support}')" = 1 ] || exit 1 [ "$($TMUX display-message -p '#{sixel_support}')" = 1 ] || exit 1
# Older Chafa chunks are independently padded, with nonzero unused bits. # Older Chafa chunks are independently padded, with nonzero unused bits.

133
regress/image_scroll.py Normal file
View File

@@ -0,0 +1,133 @@
"""PTY checks adapted from the PR's image scroll regressions."""
import re
from image_test import image_test, sixel, check_image
from image_terminal import Terminal
MARKER = b'SCROLLMARKER'
WIRE = sixel(16, 16, [(100, 0, 0), (0, 100, 0)],
[x >= 8 for y in range(16) for x in range(16)])
def attach(test, scrolls, margins=None, term='xterm-256color'):
"""Choose scroll features before attaching a client."""
features = [term, 'imagescroll' if scrolls else 'imagescroll@']
if margins is not None:
features.append('margins' if margins else 'margins@')
test.run('set-option', '-as', 'terminal-features', ':'.join(features))
client = test.attach(term=term)
prefix = str(client.pid) + ':'
actual = next(line[len(prefix):] for line in test.run('list-clients',
'-F', '#{client_pid}:#{client_termfeatures}').splitlines()
if line.startswith(prefix)).split(',')
assert ('imagescroll' in actual) == scrolls, actual
if margins is not None:
assert ('margins' in actual) == margins, actual
return client
def place(test):
"""Place marker text and a two-colour image away from the scroll edges."""
test.emit(b'\033[7;1H' + MARKER + b'\033[9;3H' + WIRE + b'\033[16;1H')
for client in test.clients:
images = client.images()
assert len(images) == 1, 'initial image was not sent'
check_image(images[0], 16, 16,
[(255, 0, 0) if x < 8 else (0, 255, 0)
for y in range(16) for x in range(16)])
def check_output(client, replay):
"""Check whether scroll damage replayed the image and surrounding text."""
data = bytes(client.data)
if replay:
assert client.images(), 'image was not replayed after a manual repaint'
assert MARKER in data, 'scroll damage did not repaint surrounding text'
else:
assert not client.images(), 'native scroll replayed the image'
assert MARKER not in data, 'native scroll repainted existing text'
return data
def check_position(test, mixed=False, down=False):
"""A later full redraw must show the scrolled image at its new position."""
test.redraw()
row = 9 if down else 7
expected = {(x, y): (255, 0, 0) if x < 24 else (0, 255, 0)
for y in range(row * 16, (row + 1) * 16) for x in range(16, 32)
if not mixed or x >= 24}
for client in test.clients:
terminal = Terminal()
terminal.feed(client.data)
terminal.check(expected)
def check_scroll(tmux, scrolls, margins=None, no_erase=False, mixed=False,
command='linefeed'):
"""Check one client's native scroll or fallback repaint."""
with image_test(tmux) as test:
test.run('set-option', '-w', 'automatic-rename', 'off')
if margins is not None:
test.run('set-option', '-w', 'pane-scrollbars', 'on')
client = attach(test, scrolls, margins)
place(test)
if no_erase:
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
operation = {'linefeed': b'\n', 'up': b'\033[S', 'down': b'\033[T',
'reverse': b'\033[H\033M'}[command]
if mixed:
# An ordinary overwrite must prevent skipping merged scroll damage.
operation += b'\033[8;3HX'
test.emit(operation)
replay = not scrolls or margins is False or mixed
data = check_output(client, replay)
if no_erase:
# Erasing the newly exposed bottom row is valid. Check that
# none of the moved image pixels were erased or drawn over.
terminal.feed(data)
terminal.check({(x, y): (255, 0, 0) if x < 24 else (0, 255, 0)
for y in range(7 * 16, 8 * 16) for x in range(16, 32)})
if scrolls and margins:
assert re.search(rb'\033\[\d+;\d+s', data), (
'no horizontal scrolling region was set', data)
check_position(test, mixed, command in ('down', 'reverse'))
def check_clients(tmux, narrow=False):
"""Check different client capabilities and shared fallback damage."""
with image_test(tmux) as test:
test.run('set-option', '-w', 'automatic-rename', 'off')
if narrow:
test.run('set-option', '-w', 'pane-scrollbars', 'on')
native = attach(test, True, True)
repaint = attach(test, True, False, term='xterm')
else:
native = attach(test, True)
repaint = attach(test, False, term='xterm')
place(test)
test.emit(b'\n')
# A large fallback repaint adds ordinary window damage, which must
# be drawn on both clients even if the first scrolled natively.
check_output(native, narrow)
check_output(repaint, True)
check_position(test)
def check_capability(tmux, enabled):
"""Iscrl grants image scrolling, and Iscrl@ overrides an enabled feature."""
with image_test(tmux) as test:
test.run('set-option', '-w', 'automatic-rename', 'off')
capability = 'Iscrl' if enabled else 'Iscrl@'
test.run('set-option', '-as', 'terminal-overrides',
'xterm-256color:' + capability)
if not enabled:
test.run('set-option', '-as', 'terminal-features',
'xterm-256color:imagescroll')
client = test.attach()
place(test)
test.emit(b'\n')
check_output(client, not enabled)
check_position(test)

192
regress/image_terminal.py Normal file
View File

@@ -0,0 +1,192 @@
"""Observe cursor positions, colours and erases in tmux's terminal output."""
import re
import unicodedata
from image_test import decode_sixel
class Terminal:
"""Track image pixels and text cells for the controls used by these tests."""
def __init__(self, sx=40, sy=16, cw=8, ch=16):
self.sx, self.sy, self.cw, self.ch = sx, sy, cw, ch
self.x = self.y = 0
self.top, self.bottom = 0, sy - 1
self.fg = self.bg = None
self.bold = False
self.pixels = {}
self.cells = {}
self.pending = b''
def erase(self, x, y, w, h):
"""Remove image pixels and text in a rectangle of cells."""
for px, py in list(self.pixels):
if x * self.cw <= px < (x + w) * self.cw and \
y * self.ch <= py < (y + h) * self.ch:
del self.pixels[px, py]
for cy in range(y, y + h):
for cx in range(x, x + w):
self.cells[cx, cy] = (' ', self.fg, self.bg, self.bold)
def sgr(self, args):
"""Apply text colours and bold attributes."""
i = 0
while i < len(args):
value = args[i]
i += 1
if value == 0:
self.fg = self.bg = None
self.bold = False
elif value in (1, 22):
self.bold = value == 1
elif value in (39, 49):
setattr(self, 'fg' if value == 39 else 'bg', None)
elif 30 <= value <= 37 or 40 <= value <= 47:
setattr(self, 'fg' if value < 40 else 'bg', value % 10)
elif 90 <= value <= 97 or 100 <= value <= 107:
setattr(self, 'fg' if value < 100 else 'bg', value % 10 + 8)
elif value in (38, 48):
mode = args[i]
i += 1
assert mode in (2, 5), args
n = 3 if mode == 2 else 1
colour = tuple(args[i:i + n]) if n == 3 else args[i]
setattr(self, 'fg' if value == 38 else 'bg', colour)
i += n
def scroll(self, top, bottom, n):
"""Move pixels and text within a vertical scroll region."""
moved = {}
for (x, y), colour in list(self.pixels.items()):
if top * self.ch <= y < (bottom + 1) * self.ch:
del self.pixels[x, y]
if top * self.ch <= y - n * self.ch < (bottom + 1) * self.ch:
moved[x, y - n * self.ch] = colour
cells = {(x, y - n): cell for (x, y), cell in self.cells.items()
if top <= y <= bottom and top <= y - n <= bottom}
self.erase(0, top, self.sx, bottom - top + 1)
self.pixels.update(moved)
self.cells.update(cells)
def csi(self, params, command):
"""Apply cursor movement and erases; ignore terminal mode controls."""
if params.startswith((b'?', b'>', b'=')) or b' ' in params:
return
args = [int(p or b'0') for p in params.split(b';')]
n = args[0] or 1
if command in b'Hf':
self.y = n - 1
self.x = (args[1] or 1) - 1 if len(args) > 1 else 0
elif command == ord('G'):
self.x = n - 1
elif command == ord('d'):
self.y = n - 1
elif command in b'ABCD':
dx, dy = {65: (0, -n), 66: (0, n),
67: (n, 0), 68: (-n, 0)}[command]
self.x = max(0, self.x + dx)
self.y = max(0, self.y + dy)
elif command == ord('X'):
self.erase(self.x, self.y, n, 1)
elif command == ord('K'):
start = self.x if args[0] == 0 else 0
end = self.x + 1 if args[0] == 1 else self.sx
self.erase(start, self.y, end - start, 1)
elif command == ord('J'):
if args[0] in (2, 3):
self.erase(0, 0, self.sx, self.sy)
elif args[0] == 0:
self.erase(self.x, self.y, self.sx - self.x, 1)
self.erase(0, self.y + 1, self.sx, self.sy - self.y - 1)
elif args[0] == 1:
self.erase(0, 0, self.sx, self.y)
self.erase(0, self.y, self.x + 1, 1)
elif command == ord('m'):
self.sgr(args)
elif command == ord('r'):
self.top = n - 1
self.bottom = (args[1] or self.sy) - 1 if len(args) > 1 else self.sy - 1
self.x = self.y = 0
elif command in b'STLM':
top = self.y if command in b'LM' else self.top
self.scroll(top, self.bottom, n if command in b'SM' else -n)
else:
assert command in b'hlcqnt', (params, chr(command))
def feed(self, data):
"""Apply complete output sequences, keeping any unfinished sequence."""
data = self.pending + bytes(data)
pos = 0
while pos < len(data):
rest = data[pos:]
if rest.startswith((b'\033P', b'\033]')):
match = re.match(rb'\033([P\]])(.*?)(?:\007|\033\\)',
rest, re.S)
if match is None:
break
if match[1] == b'P':
image = re.match(rb'([0-9;]*)q(.*)', match[2], re.S)
if image is not None:
params = image[1].split(b';')
p2 = int(params[1] or b'0') if len(params) > 1 else 0
w, h, pixels = decode_sixel(image[2], p2)
for i, colour in enumerate(pixels):
if colour is not None:
self.pixels[self.x * self.cw + i % w,
self.y * self.ch + i // w] = colour
pos += match.end()
elif rest.startswith(b'\033['):
match = re.match(rb'\033\[([0-?]*[ -/]*)([@-~])', rest)
if match is None:
break
self.csi(match[1], match[2][0])
pos += match.end()
elif rest[0] == 27:
match = re.match(rb'\033(?:[()][0-~]|.)', rest, re.S)
if match is None:
break
assert match[0] in (b'\033(B', b'\033(0', b'\033)B',
b'\033=', b'\033>', b'\033\\'), match[0]
pos += match.end()
elif rest[0] < 32 or rest[0] == 127:
if rest[0] == 13:
self.x = 0
elif rest[0] == 10:
if self.y == self.bottom:
self.scroll(self.top, self.bottom, 1)
else:
self.y += 1
elif rest[0] == 8:
self.x = max(0, self.x - 1)
pos += 1
else:
if rest[0] < 128:
n = 1
elif rest[0] < 224:
n = 2
elif rest[0] < 240:
n = 3
else:
n = 4
if len(rest) < n:
break
char = rest[:n].decode()
width = 2 if unicodedata.east_asian_width(char) in 'WF' else 1
if self.x >= self.sx:
self.x = 0
self.y += 1
self.erase(self.x, self.y, width, 1)
self.cells[self.x, self.y] = (char, self.fg, self.bg, self.bold)
self.x += width
pos += n
self.pending = data[pos:]
def check(self, expected):
"""Compare all visible image pixels, including their positions."""
assert self.pixels.keys() == expected.keys(), (
'pixel positions differ', len(self.pixels), len(expected),
list(self.pixels.keys() - expected.keys())[:5],
list(expected.keys() - self.pixels.keys())[:5])
for point, colour in expected.items():
assert max(abs(a - b) for a, b in zip(self.pixels[point], colour)) \
<= 3, (point, self.pixels[point], colour)

307
regress/image_test.py Normal file
View File

@@ -0,0 +1,307 @@
"""Helpers for image tests using an attached client on a PTY."""
import contextlib
import colorsys
import errno
import fcntl
import itertools
import os
import re
import select
import signal
import struct
import subprocess
import sys
import termios
import time
class Client:
"""An attached client and the bytes written to its terminal."""
def __init__(self, pid, fd):
self.pid = pid
self.fd = fd
self.data = bytearray()
def read(self, duration=0.1):
"""Read terminal output for a bounded time."""
end = time.monotonic() + duration
while time.monotonic() < end:
if not select.select([self.fd], [], [], 0.01)[0]:
continue
try:
data = os.read(self.fd, 65536)
except OSError as e:
if e.errno == errno.EIO:
raise RuntimeError("attached client exited") from e
raise
if not data:
raise RuntimeError("attached client exited")
self.data.extend(data)
def clear(self):
"""Finish reading earlier output before starting a new check."""
self.read()
self.data.clear()
def images(self):
"""Decode complete SIXEL sequences from the terminal output."""
images = []
for params, body in re.findall(
rb"\033P([0-9;]*)q(.*?)\033\\", self.data, re.S):
params = params.split(b";")
p2 = int(params[1] or b"0") if len(params) > 1 else 0
images.append(decode_sixel(body, p2))
return images
class ImageTest:
"""A server with one pane which copies input directly to its output."""
serial = itertools.count()
def __init__(self, tmux, sx=40, sy=16):
# Short, unique socket labels also fit macOS's socket path limit.
self.server = [tmux, "-Lis%d-%d" % (os.getpid(), next(self.serial)),
"-f/dev/null"]
self.sx = sx
self.sy = sy
self.clients = []
self.marker = 0
def run(self, *args, check=True, input=None):
"""Run a command on this server."""
p = subprocess.run(self.server + list(args), capture_output=True,
input=input, timeout=10)
if check and p.returncode != 0:
raise RuntimeError("%r: %s" % (args, p.stderr.decode()))
return p.stdout.decode().strip()
def wait(self, predicate):
"""Read client output while waiting for a server state change."""
end = time.monotonic() + 10
while True:
for client in self.clients:
client.read(0.01)
if predicate():
return
if time.monotonic() >= end:
raise RuntimeError("timed out waiting for server state")
time.sleep(0.01)
def start(self):
"""Start the pane and skip when the server has no image support."""
self.run("new-session", "-d", "-s", "image", "-x", str(self.sx),
"-y", str(self.sy), "stty raw -echo; "
"printf '\033]2;READY\007'; exec cat")
support = self.run("display-message", "-p", "#{image_support}")
if support == "0":
sys.exit(101)
assert support == "1", ("unexpected image_support", support)
self.run("set-option", "-g", "status", "off")
self.wait(lambda: self.run("display-message", "-p",
"#{pane_title}") == "READY")
def attach(self, graphical=True, utf8=True, cw=8, ch=16, term=None):
"""Attach a client with a known cell pixel size and image feature."""
if term is None:
term = "xterm-256color" if graphical else "screen-256color"
features = term + (":sixel" if graphical else ":sixel@")
self.run("set-option", "-as", "terminal-features", features)
pid, fd = os.forkpty()
if pid == 0:
try:
os.environ.pop("TMUX", None)
os.environ["TERM"] = term
os.environ["LC_ALL"] = "C.UTF-8" if utf8 else "C"
fcntl.ioctl(0, termios.TIOCSWINSZ, struct.pack("HHHH",
self.sy, self.sx, self.sx * cw, self.sy * ch))
args = self.server + (["-u"] if utf8 else [])
os.execv(self.server[0], args +
["attach-session", "-t", "image"])
finally:
os._exit(1)
client = Client(pid, fd)
self.clients.append(client)
self.wait(lambda: any(line.startswith(str(pid) + ":") for line in
self.run("list-clients", "-F", "#{client_pid}:").splitlines()))
# Answer DA so the startup timeout cannot schedule a later redraw.
os.write(fd, b"\033[?65;4c" if graphical else b"\033[?65c")
self.wait(lambda: self.client_size(client) ==
"%dx%d:%dx%d" % (self.sx, self.sy, cw, ch))
if len(self.clients) == 1 and cw != 0 and ch != 0:
self.wait(lambda: self.run("display-message", "-p",
"#{window_cell_width}x#{window_cell_height}") ==
"%dx%d" % (cw, ch))
client.name = next(line.split(":", 1)[1] for line in
self.run("list-clients", "-F",
"#{client_pid}:#{client_name}").splitlines()
if line.startswith(str(pid) + ":"))
client.read(0.2)
client.clear()
return client
def client_size(self, client):
"""Get the character and cell pixel size of one client."""
prefix = str(client.pid) + ":"
return next((line[len(prefix):] for line in self.run("list-clients",
"-F", "#{client_pid}:#{client_width}x#{client_height}:"
"#{client_cell_width}x#{client_cell_height}").splitlines()
if line.startswith(prefix)), None)
def send(self, data):
"""Send literal bytes to the pane."""
if len(data) > 512:
self.run("load-buffer", "-b", "image", "-", input=data)
self.run("paste-buffer", "-b", "image", "-S", "-r", "-d")
else:
self.run("send-keys", "-H", *["%02x" % ch for ch in data])
def emit(self, data):
"""Send pane output and wait until its trailing title is parsed."""
for client in self.clients:
client.clear()
self.marker += 1
title = "image-%d" % self.marker
self.send(data + b"\033]2;" + title.encode() + b"\007")
self.wait(lambda: self.run("display-message", "-p",
"#{pane_title}") == title)
for client in self.clients:
client.read(0.2)
def redraw(self):
"""Request a full redraw and collect its terminal output."""
for client in self.clients:
client.clear()
for client in self.clients:
self.run("refresh-client", "-t", client.name)
for client in self.clients:
client.read(0.2)
def resize(self, client, sx, sy, cw=8, ch=16):
"""Resize one client's terminal and wait for the window size."""
client.clear()
fcntl.ioctl(client.fd, termios.TIOCSWINSZ,
struct.pack("HHHH", sy, sx, sx * cw, sy * ch))
os.kill(client.pid, signal.SIGWINCH)
self.wait(lambda: self.client_size(client) ==
"%dx%d:%dx%d" % (sx, sy, cw, ch))
self.wait(lambda: self.run("display-message", "-p",
"#{window_width}x#{window_height}") == "%dx%d" % (sx, sy))
client.read(0.2)
def close(self):
"""Stop the server and reap every attached client."""
self.run("kill-server", check=False)
for client in self.clients:
os.close(client.fd)
if os.waitpid(client.pid, os.WNOHANG) == (0, 0):
os.kill(client.pid, signal.SIGTERM)
os.waitpid(client.pid, 0)
@contextlib.contextmanager
def image_test(tmux, sx=40, sy=16):
"""Clean up the server even when a test fails or skips."""
test = ImageTest(tmux, sx, sy)
try:
test.start()
yield test
finally:
test.close()
def sixel(w, h, palette, pixels, p2=1):
"""Build SIXEL input from a palette and row-major palette indexes."""
assert len(pixels) == w * h
out = ['\033P9;%dq"1;1;%d;%d' % (p2, w, h)]
for c, rgb in enumerate(palette):
out.append("#%d;2;%d;%d;%d" % (c, *rgb))
for y in range(0, h, 6):
planes = []
for c in range(len(palette)):
columns = []
for x in range(w):
bits = sum(1 << bit for bit in range(min(6, h - y))
if pixels[(y + bit) * w + x] == c)
columns.append(chr(0x3f + bits))
if all(ch == "?" for ch in columns):
continue
plane = "#%d" % c
for ch, run in itertools.groupby(columns):
n = len(list(run))
plane += "!%d%s" % (n, ch) if n > 3 else ch * n
planes.append(plane)
out.append("$".join(planes))
if y + 6 < h:
out.append("-")
out.append("\033\\")
return "".join(out).encode()
def decode_sixel(body, p2=1):
"""Read RGB output pixels without using tmux's SIXEL decoder."""
attributes = re.match(rb'"1;1;(\d+);(\d+)', body)
assert attributes is not None, ("missing raster attributes", body[:80])
w, h = map(int, attributes.groups())
pixels = [None if p2 == 1 else (0, 0, 0)] * (w * h)
palette = {}
c = x = y = 0
pos = attributes.end()
while pos < len(body):
ch = body[pos]
pos += 1
if ch == ord("#"):
match = re.match(rb"(\d+)(?:;([12]);(\d+);(\d+);(\d+))?", body[pos:])
assert match is not None, body[pos:pos + 80]
c = int(match[1])
if match[2] is not None:
values = tuple(int(match[i]) for i in range(3, 6))
if match[2] == b"1":
hue, lightness, saturation = values
rgb = colorsys.hls_to_rgb(((hue + 240) % 360) / 360,
lightness / 100, saturation / 100)
palette[c] = tuple(int(v * 255 + .5) for v in rgb)
else:
assert all(v <= 100 for v in values), values
palette[c] = tuple((v * 255 + 50) // 100 for v in values)
pos += match.end()
continue
if ch == ord("$"):
x = 0
continue
if ch == ord("-"):
x = 0
y += 6
continue
n = 1
if ch == ord("!"):
match = re.match(rb"(\d+)(.)", body[pos:], re.S)
assert match is not None
n = int(match[1])
ch = match[2][0]
pos += match.end()
assert 0x3f <= ch <= 0x7e, ch
for bit in range(6):
if not (ch - 0x3f) & (1 << bit):
continue
assert y + bit < h and x + n <= w, (x, y, n, bit, w, h)
assert c in palette, ("undefined output colour", c)
for xx in range(x, x + n):
pixels[(y + bit) * w + xx] = palette[c]
x += n
return w, h, pixels
def check_image(image, w, h, expected):
"""Compare decoded output with expected RGB pixels and transparency."""
assert image[:2] == (w, h), (image[:2], (w, h))
assert len(expected) == w * h
for i, (actual, wanted) in enumerate(zip(image[2], expected)):
if wanted is None:
assert actual is None, (i % w, i // w, actual, wanted)
else:
assert actual is not None, (i % w, i // w, actual, wanted)
assert max(abs(a - b) for a, b in zip(actual, wanted)) <= 3, (
i % w, i // w, actual, wanted)

View File

@@ -9,7 +9,7 @@ TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 - "$TEST_TMUX" <<'PY' python3 -B - "$TEST_TMUX" <<'PY'
import fcntl import fcntl
import os import os
import re import re

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-redraw.c,v 1.163 2026/10/02 12:48:52 nicm Exp $ */ /* $OpenBSD: screen-redraw.c,v 1.164 2026/10/14 10:34:14 nicm Exp $ */
/* /*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -98,11 +98,12 @@ enum redraw_span_type {
#define REDRAW_ALL 0x7fffffff #define REDRAW_ALL 0x7fffffff
#define REDRAW_IS_ALL(flags) ((flags) == REDRAW_ALL) #define REDRAW_IS_ALL(flags) ((flags) == REDRAW_ALL)
enum redraw_image_phase { enum redraw_phase {
REDRAW_IMAGES_BEFORE, REDRAW_IMAGES_BEFORE,
REDRAW_TEXT, REDRAW_TEXT,
REDRAW_IMAGES_AFTER REDRAW_IMAGES_AFTER
}; };
#define REDRAW_PHASE_COUNT 3
/* UTF-8 isolate characters. */ /* UTF-8 isolate characters. */
#define REDRAW_START_ISOLATE "\342\201\246" #define REDRAW_START_ISOLATE "\342\201\246"
@@ -216,10 +217,11 @@ struct redraw_damage {
u_int y; u_int y;
u_int sx; u_int sx;
u_int sy; u_int sy;
int flags; int flags;
#define REDRAW_DAMAGE_SCROLL 0x1 #define REDRAW_DAMAGE_SCROLL 0x1
TAILQ_ENTRY(redraw_damage) entry; TAILQ_ENTRY(redraw_damage) entry;
}; };
/* /*
@@ -1109,9 +1111,7 @@ redraw_free_damage(struct window *w)
free(rd); free(rd);
} }
w->damage_count = 0; w->damage_count = 0;
#ifdef ENABLE_IMAGES w->scroll_pending = 0;
w->image_scroll_pending = 0;
#endif
} }
/* Collapse all pending damage for a window into one rectangle. */ /* Collapse all pending damage for a window into one rectangle. */
@@ -1154,7 +1154,7 @@ redraw_collapse_damage(struct window *w)
/* Record window damage, merging nearby rectangles and limiting the count. */ /* Record window damage, merging nearby rectangles and limiting the count. */
static void static void
redraw_damage_window_flags(struct window *w, u_int x, u_int y, u_int sx, redraw_apply_window_damage(struct window *w, u_int x, u_int y, u_int sx,
u_int sy, int flags) u_int sy, int flags)
{ {
struct redraw_damage *rd; struct redraw_damage *rd;
@@ -1171,9 +1171,8 @@ redraw_damage_window_flags(struct window *w, u_int x, u_int y, u_int sx,
TAILQ_FOREACH(rd, &w->damage, entry) { TAILQ_FOREACH(rd, &w->damage, entry) {
if (x > rd->x + rd->sx || rd->x > x + sx || if (x > rd->x + rd->sx || rd->x > x + sx ||
y > rd->y + rd->sy || rd->y > y + sy) { y > rd->y + rd->sy || rd->y > y + sy)
continue; continue;
}
x0 = (x < rd->x) ? x : rd->x; x0 = (x < rd->x) ? x : rd->x;
y0 = (y < rd->y) ? y : rd->y; y0 = (y < rd->y) ? y : rd->y;
@@ -1208,44 +1207,44 @@ redraw_damage_window_flags(struct window *w, u_int x, u_int y, u_int sx,
void void
redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy) redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy)
{ {
redraw_damage_window_flags(w, x, y, sx, sy, 0); redraw_apply_window_damage(w, x, y, sx, sy, 0);
} }
#ifdef ENABLE_IMAGES /* Record damage caused by terminal scrolling. */
/* Record damage caused by an image moving with a terminal scroll. */
void void
redraw_damage_window_scroll(struct window *w, u_int x, u_int y, u_int sx, redraw_damage_scroll(struct window *w, u_int x, u_int y, u_int sx, u_int sy)
u_int sy)
{ {
if (!w->image_scroll_pending) { if (!w->scroll_pending) {
w->image_scroll_epoch++; w->scroll_generation++;
w->image_scroll_pending = 1; w->scroll_pending = 1;
} }
redraw_damage_window_flags(w, x, y, sx, sy, REDRAW_DAMAGE_SCROLL); redraw_apply_window_damage(w, x, y, sx, sy, REDRAW_DAMAGE_SCROLL);
} }
/* Remember whether this client used terminal scrolling for the damage. */ /* Remember whether this client used terminal scrolling for the damage. */
void void
redraw_image_scroll_result(struct tty *tty, const struct tty_ctx *ctx, redraw_scroll_result(struct tty *tty, const struct tty_ctx *ctx, int failed)
int failed)
{ {
struct window_pane *wp = ctx->arg; struct window_pane *wp = ctx->arg;
struct window *w; struct window *w;
if (wp == NULL || (w = wp->window) == NULL || !w->image_scroll_pending) if (wp == NULL)
return; return;
if (tty->image_scroll_window != w || w = wp->window;
tty->image_scroll_epoch != w->image_scroll_epoch) { if (!w->scroll_pending)
tty->image_scroll_window = w; return;
tty->image_scroll_epoch = w->image_scroll_epoch; if (tty->scroll_window != w ||
tty->image_scroll_failed = 0; tty->scroll_generation != w->scroll_generation) {
tty->scroll_window = w;
tty->scroll_generation = w->scroll_generation;
tty->scroll_failed = 0;
} }
if (failed) if (failed)
tty->image_scroll_failed = 1; tty->scroll_failed = 1;
} }
#endif
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
/* XXX not sure if they is needed */
/* Record damage for every pane border-status row in a window. */ /* Record damage for every pane border-status row in a window. */
static void static void
redraw_damage_window_pane_status(struct window *w) redraw_damage_window_pane_status(struct window *w)
@@ -1312,8 +1311,7 @@ redraw_get_scene(struct client *c)
/* Draw a pane span. */ /* Draw a pane span. */
static void static void
redraw_draw_pane_span(struct redraw_draw_ctx *dctx, redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
struct redraw_span *span, u_int x, u_int y, u_int n, struct redraw_span *span, u_int x, u_int y, u_int n)
enum redraw_image_phase phase)
{ {
struct redraw_scene *scene = dctx->scene; struct redraw_scene *scene = dctx->scene;
struct client *c = scene->c; struct client *c = scene->c;
@@ -1331,15 +1329,7 @@ redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
px = span->data.p.px + (x - span->x); px = span->data.p.px + (x - span->x);
py = span->data.p.py; py = span->data.p.py;
#ifdef ENABLE_IMAGES tty_draw_line(tty, s, px, py, n, x, y, &style_ctx);
if (phase != REDRAW_TEXT) {
image_draw_line(tty, s, px, py, n, x, y,
phase == REDRAW_IMAGES_BEFORE);
return;
}
#endif
if (phase == REDRAW_TEXT)
tty_draw_line(tty, s, px, py, n, x, y, &style_ctx);
} }
/* Get default border style for spans without a pane. */ /* Get default border style for spans without a pane. */
@@ -1594,10 +1584,11 @@ redraw_draw_menu_span(struct redraw_draw_ctx *dctx,
/* Draw a span. */ /* Draw a span. */
static void static void
redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span, redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
u_int y, enum redraw_image_phase phase) u_int y, enum redraw_phase phase)
{ {
struct redraw_span_data *data = &span->data; struct redraw_span_data *data = &span->data;
enum redraw_span_type type = data->type; enum redraw_span_type type = data->type;
u_int width = span->width;
if (type == REDRAW_SPAN_STATUS && ~data->st.wp->flags & PANE_NEWSTATUS) if (type == REDRAW_SPAN_STATUS && ~data->st.wp->flags & PANE_NEWSTATUS)
return; return;
@@ -1606,22 +1597,29 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
switch (span->data.type) { switch (span->data.type) {
case REDRAW_SPAN_PANE: case REDRAW_SPAN_PANE:
redraw_draw_pane_span(dctx, span, span->x, y, span->width, if (phase == REDRAW_TEXT)
phase); redraw_draw_pane_span(dctx, span, span->x, y, width);
#ifdef ENABLE_IMAGES
else {
image_draw_line(&dctx->scene->c->tty, data->p.wp->screen,
data->p.px, data->p.py, width, span->x, y,
phase == REDRAW_IMAGES_BEFORE);
}
#endif
break; break;
case REDRAW_SPAN_BORDER: case REDRAW_SPAN_BORDER:
case REDRAW_SPAN_EMPTY: case REDRAW_SPAN_EMPTY:
case REDRAW_SPAN_OUTSIDE: case REDRAW_SPAN_OUTSIDE:
redraw_draw_border_span(dctx, span, span->x, y, span->width); redraw_draw_border_span(dctx, span, span->x, y, width);
break; break;
case REDRAW_SPAN_STATUS: case REDRAW_SPAN_STATUS:
redraw_draw_status_span(dctx, span, span->x, y, span->width); redraw_draw_status_span(dctx, span, span->x, y, width);
break; break;
case REDRAW_SPAN_SCROLLBAR: case REDRAW_SPAN_SCROLLBAR:
redraw_draw_scrollbar_span(dctx, span, span->x, y, span->width); redraw_draw_scrollbar_span(dctx, span, span->x, y, width);
break; break;
case REDRAW_SPAN_MENU: case REDRAW_SPAN_MENU:
redraw_draw_menu_span(dctx, span, span->x, y, span->width); redraw_draw_menu_span(dctx, span, span->x, y, width);
break; break;
} }
} }
@@ -1635,7 +1633,7 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
struct redraw_line *line; struct redraw_line *line;
struct redraw_spans *spans; struct redraw_spans *spans;
struct redraw_span *span; struct redraw_span *span;
enum redraw_image_phase phase; enum redraw_phase phase;
u_int cy; u_int cy;
int y, top, bottom; int y, top, bottom;
@@ -1669,8 +1667,7 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
#endif #endif
/* Draw images behind text, then text, then images above it. */ /* Draw images behind text, then text, then images above it. */
for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER; for (phase = 0; phase < REDRAW_PHASE_COUNT; phase++) {
phase++) {
for (y = top; y < bottom; y++) { for (y = top; y < bottom; y++) {
line = &scene->lines[y]; line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP) if (dctx->flags & REDRAW_STATUS_TOP)
@@ -1680,20 +1677,18 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
if (flags & REDRAW_PANE) { if (flags & REDRAW_PANE) {
spans = &line->spans[REDRAW_SPAN_PANE]; spans = &line->spans[REDRAW_SPAN_PANE];
TAILQ_FOREACH(span, spans, entry) { TAILQ_FOREACH(span, spans, entry) {
if (span->data.p.wp == wp) { if (span->data.p.wp != wp)
redraw_draw_span(dctx, span, cy, continue;
phase); redraw_draw_span(dctx, span, cy, phase);
}
} }
} }
if (phase == REDRAW_TEXT && if (phase == REDRAW_TEXT &&
(flags & REDRAW_PANE_SCROLLBAR)) { (flags & REDRAW_PANE_SCROLLBAR)) {
spans = &line->spans[REDRAW_SPAN_SCROLLBAR]; spans = &line->spans[REDRAW_SPAN_SCROLLBAR];
TAILQ_FOREACH(span, spans, entry) { TAILQ_FOREACH(span, spans, entry) {
if (span->data.sb.wp == wp) { if (span->data.sb.wp != wp)
redraw_draw_span(dctx, span, cy, continue;
phase); redraw_draw_span(dctx, span, cy, phase);
}
} }
} }
} }
@@ -1707,6 +1702,36 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
#endif #endif
} }
/* Return whether to draw a span type in this phase for these flags. */
static int
redraw_should_draw(enum redraw_span_type type, int flags,
enum redraw_phase phase)
{
if (phase != REDRAW_TEXT && type != REDRAW_SPAN_PANE)
return (0);
if (REDRAW_IS_ALL(flags))
return (1);
switch (type) {
case REDRAW_SPAN_PANE:
return (flags & REDRAW_PANE);
case REDRAW_SPAN_OUTSIDE:
return (flags & REDRAW_OUTSIDE);
case REDRAW_SPAN_EMPTY:
return (flags & REDRAW_EMPTY);
case REDRAW_SPAN_BORDER:
return (flags & REDRAW_PANE_BORDER);
case REDRAW_SPAN_STATUS:
return (flags & REDRAW_PANE_STATUS);
case REDRAW_SPAN_SCROLLBAR:
return (flags & REDRAW_PANE_SCROLLBAR);
case REDRAW_SPAN_MENU:
return (flags & REDRAW_MENU);
default:
return (0);
}
}
/* Draw lines. */ /* Draw lines. */
static void static void
redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags) redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
@@ -1715,12 +1740,11 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
struct redraw_line *line; struct redraw_line *line;
struct redraw_spans *spans; struct redraw_spans *spans;
struct redraw_span *span; struct redraw_span *span;
enum redraw_image_phase phase; enum redraw_phase phase;
u_int y, cy, type; u_int y, cy, type;
/* Draw each image layer around the text spans. */ /* Draw each phase. */
for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER; for (phase = 0; phase < REDRAW_PHASE_COUNT; phase++) {
phase++) {
for (y = 0; y < scene->sy; y++) { for (y = 0; y < scene->sy; y++) {
line = &scene->lines[y]; line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP) if (dctx->flags & REDRAW_STATUS_TOP)
@@ -1728,44 +1752,8 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
else else
cy = y; cy = y;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) { for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (phase != REDRAW_TEXT && if (!redraw_should_draw(type, flags, phase))
type != REDRAW_SPAN_PANE) {
continue; continue;
}
if (!REDRAW_IS_ALL(flags)) {
switch (type) {
case REDRAW_SPAN_PANE:
if (~flags & REDRAW_PANE)
continue;
break;
case REDRAW_SPAN_OUTSIDE:
if (~flags & REDRAW_OUTSIDE)
continue;
break;
case REDRAW_SPAN_EMPTY:
if (~flags & REDRAW_EMPTY)
continue;
break;
case REDRAW_SPAN_BORDER:
if (~flags & REDRAW_PANE_BORDER)
continue;
break;
case REDRAW_SPAN_STATUS:
if (~flags & REDRAW_PANE_STATUS)
continue;
break;
case REDRAW_SPAN_SCROLLBAR:
if (~flags & REDRAW_PANE_SCROLLBAR)
continue;
break;
case REDRAW_SPAN_MENU:
if (~flags & REDRAW_MENU)
continue;
break;
default:
continue;
}
}
spans = &line->spans[type]; spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry) TAILQ_FOREACH(span, spans, entry)
redraw_draw_span(dctx, span, cy, phase); redraw_draw_span(dctx, span, cy, phase);
@@ -2050,7 +2038,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
if ((flags & REDRAW_PANE) && if ((flags & REDRAW_PANE) &&
(image_backend_flags(tty) & (image_backend_get_flags(tty) &
(IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED)) == (IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED)) ==
IMAGE_BACKEND_GRAPHICAL) { IMAGE_BACKEND_GRAPHICAL) {
redraw_damage_window_pane_status(scene->w); redraw_damage_window_pane_status(scene->w);
@@ -2216,7 +2204,7 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
struct redraw_line *line; struct redraw_line *line;
struct redraw_spans *spans; struct redraw_spans *spans;
struct redraw_span *span; struct redraw_span *span;
enum redraw_image_phase phase; enum redraw_phase phase;
u_int cy, yy, type; u_int cy, yy, type;
if (x >= scene->sx || y >= scene->sy) if (x >= scene->sx || y >= scene->sy)
@@ -2253,8 +2241,7 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
#endif #endif
/* Compose image layers and text across the damaged spans. */ /* Compose image layers and text across the damaged spans. */
for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER; for (phase = 0; phase < REDRAW_PHASE_COUNT; phase++) {
phase++) {
for (yy = y; yy < y + sy; yy++) { for (yy = y; yy < y + sy; yy++) {
line = &scene->lines[yy]; line = &scene->lines[yy];
if (dctx->flags & REDRAW_STATUS_TOP) if (dctx->flags & REDRAW_STATUS_TOP)
@@ -2263,9 +2250,8 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
cy = yy; cy = yy;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) { for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (phase != REDRAW_TEXT && if (phase != REDRAW_TEXT &&
type != REDRAW_SPAN_PANE) { type != REDRAW_SPAN_PANE)
continue; continue;
}
if (type == REDRAW_SPAN_STATUS) if (type == REDRAW_SPAN_STATUS)
continue; continue;
spans = &line->spans[type]; spans = &line->spans[type];
@@ -2310,46 +2296,17 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
} }
} }
/* Compose one damaged rectangle, clipped to what this client can see. */
static void
redraw_client_damage_rect(struct client *c, struct redraw_draw_ctx *dctx,
__unused struct window *w, u_int ox, u_int oy, u_int sx, u_int sy,
struct redraw_damage *rd)
{
u_int x0, y0, x1, y1;
#ifdef ENABLE_IMAGES
/* Skip scroll damage already handled by this terminal. */
if ((rd->flags & REDRAW_DAMAGE_SCROLL) &&
(image_backend_flags(&c->tty) & IMAGE_BACKEND_SCROLLS) &&
c->tty.image_scroll_window == w &&
c->tty.image_scroll_epoch == w->image_scroll_epoch &&
!c->tty.image_scroll_failed) {
return;
}
#endif
x0 = (rd->x > ox) ? rd->x : ox;
y0 = (rd->y > oy) ? rd->y : oy;
x1 = (rd->x + rd->sx < ox + sx) ? rd->x + rd->sx : ox + sx;
y1 = (rd->y + rd->sy < oy + sy) ? rd->y + rd->sy : oy + sy;
if (x0 >= x1 || y0 >= y1)
return;
log_debug("%s: %s composing damage %u,%u %ux%u", __func__, c->name,
x0 - ox, y0 - oy, x1 - x0, y1 - y0);
redraw_draw_damage_rectangle(dctx, x0 - ox, y0 - oy, x1 - x0,
y1 - y0);
}
/* Draw pending window damage on this client. */ /* Draw pending window damage on this client. */
void void
redraw_client_damage(struct client *c) redraw_client_damage(struct client *c)
{ {
struct window *w = c->session->curw->window; struct window *w = c->session->curw->window;
struct tty *tty = &c->tty;
struct window_pane *wp; struct window_pane *wp;
struct redraw_scene *scene; struct redraw_scene *scene;
struct redraw_draw_ctx dctx; struct redraw_draw_ctx dctx;
struct redraw_damage *rd; struct redraw_damage *rd;
u_int ox, oy, sx, sy; u_int ox, oy, sx, sy, x0, y0, x1, y1;
if (TAILQ_EMPTY(&w->damage)) if (TAILQ_EMPTY(&w->damage))
return; return;
@@ -2366,9 +2323,25 @@ redraw_client_damage(struct client *c)
wp->active_border_gc_set = 0; wp->active_border_gc_set = 0;
} }
tty_sync_start(&c->tty); tty_sync_start(tty);
tty_update_mode(&c->tty, c->tty.mode & ~CURSOR_MODES, NULL); tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL);
TAILQ_FOREACH(rd, &w->damage, entry) TAILQ_FOREACH(rd, &w->damage, entry) {
redraw_client_damage_rect(c, &dctx, w, ox, oy, sx, sy, rd); #ifdef ENABLE_IMAGES
if ((rd->flags & REDRAW_DAMAGE_SCROLL) &&
(image_backend_get_flags(tty) & IMAGE_BACKEND_SCROLLS) &&
tty->scroll_window == w &&
tty->scroll_generation == w->scroll_generation &&
!tty->scroll_failed)
continue;
#endif
x0 = (rd->x > ox) ? rd->x : ox;
y0 = (rd->y > oy) ? rd->y : oy;
x1 = (rd->x + rd->sx < ox + sx) ? rd->x + rd->sx : ox + sx;
y1 = (rd->y + rd->sy < oy + sy) ? rd->y + rd->sy : oy + sy;
if (x0 < x1 && y0 < y1) {
redraw_draw_damage_rectangle(&dctx, x0 - ox, y0 - oy,
x1 - x0, y1 - y0);
}
}
} }

View File

@@ -730,6 +730,10 @@ screen_write_fast_copy(struct screen_write_ctx *ctx, struct screen *src,
grid_get_cell(gd, xx, yy, &gc); grid_get_cell(gd, xx, yy, &gc);
if (xx + gc.data.width > px + nx) if (xx + gc.data.width > px + nx)
break; break;
#ifdef ENABLE_IMAGES
if (ctx->flags & SCREEN_WRITE_INPUT)
image_damage_screen(ctx, s->cx, s->cy, 1, 1);
#endif
grid_view_set_cell(s->grid, s->cx, s->cy, &gc); grid_view_set_cell(s->grid, s->cx, s->cy, &gc);
if (!window_position_is_visible(r, xoff + s->cx)) if (!window_position_is_visible(r, xoff + s->cx))

View File

@@ -32,53 +32,16 @@ struct screen_write_ctx;
struct sixel_image; struct sixel_image;
struct tty; struct tty;
struct grid_cell; struct grid_cell;
struct image_rect;
struct image_backend; struct image_backend;
/* Maximum image size in bytes. */
#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) #define IMAGE_SIZE_LIMIT (64 * 1024 * 1024)
/* Backend draws images as graphics. */
#define IMAGE_BACKEND_GRAPHICAL 0x1 #define IMAGE_BACKEND_GRAPHICAL 0x1
/* Backend images move with terminal scrolling. */
#define IMAGE_BACKEND_SCROLLS 0x2 #define IMAGE_BACKEND_SCROLLS 0x2
#define IMAGE_BACKEND_CLIPPED 0x4 #define IMAGE_BACKEND_CLIPPED 0x4
/* Private ACS keys used by the text image backend. */
#define TTY_ACS_IMAGE_SHADE_LIGHT 0x80
#define TTY_ACS_IMAGE_SHADE_MEDIUM 0x81
#define TTY_ACS_IMAGE_SHADE_DARK 0x82
#define TTY_ACS_IMAGE_BLOCK 0x83
#define TTY_ACS_IMAGE_HALF_UPPER 0x84
#define TTY_ACS_IMAGE_HALF_LOWER 0x85
#define TTY_ACS_IMAGE_HALF_LEFT 0x86
#define TTY_ACS_IMAGE_HALF_RIGHT 0x87
#define TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT 0x88
#define TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT 0x89
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT 0x8a
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT 0x8b
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT 0x8c
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT 0x8d
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT 0x8e
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT 0x8f
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT 0x90
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT 0x91
#define TTY_ACS_IMAGE_SEXTANT_FIRST 0x92
#define TTY_ACS_IMAGE_SEXTANT_LAST 0xcd
/* Averaged RGBA colour and brightness used by the text image backend. */
struct image_sample {
u_char red;
u_char green;
u_char blue;
u_char alpha;
u_char brightness;
};
#define IMAGE_SAMPLE_COLUMNS 2
#define IMAGE_SAMPLE_ROWS 6
/* Whole-cell and subcell samples generated lazily for one image cell. */
struct image_cell {
struct image_sample whole;
struct image_sample samples[IMAGE_SAMPLE_ROWS][IMAGE_SAMPLE_COLUMNS];
};
enum kitty_parse_status { enum kitty_parse_status {
KITTY_PARSE_ERROR = -1, KITTY_PARSE_ERROR = -1,
KITTY_PARSE_OK, KITTY_PARSE_OK,
@@ -135,6 +98,24 @@ struct image_view {
u_int sy; u_int sy;
}; };
/* A part of an image to draw. */
struct image_rect {
struct image *image;
int64_t z;
/* Size in cells. */
u_int sx;
u_int sy;
/* Source in image in cells. */
u_int src_x;
u_int src_y;
/* Destination on terminal in cells. */
u_int dst_x;
u_int dst_y;
};
/* image.c */ /* image.c */
struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int, struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int,
u_char *); u_char *);
@@ -156,10 +137,7 @@ void image_set_sixel(struct image *, struct sixel_image *);
u_int image_get_source_id(struct image *); u_int image_get_source_id(struct image *);
u_int image_get_order_id(struct image *); u_int image_get_order_id(struct image *);
void image_set_order_id(struct image *, u_int); void image_set_order_id(struct image *, u_int);
void *image_get_fallback_data(struct image *); void image_get_pixel_area(struct image *, u_int, u_int, u_int,
void image_set_fallback_data(struct image *, void *);
const struct image_cell *image_get_cell(struct image *, u_int, u_int);
void image_get_pixel_rect(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 *);
u_char *image_base64_decode(const char *, size_t, size_t, size_t *); u_char *image_base64_decode(const char *, size_t, size_t, size_t *);
u_char *image_png_decode(const u_char *, size_t, size_t, u_int *, u_char *image_png_decode(const u_char *, size_t, size_t, u_int *,
@@ -200,8 +178,10 @@ void image_write_kitty(struct screen_write_ctx *, struct image *, u_int,
void image_clear(struct screen_write_ctx *, u_int); void image_clear(struct screen_write_ctx *, u_int);
void image_clear_kitty(struct screen_write_ctx *, void image_clear_kitty(struct screen_write_ctx *,
const struct kitty_parse_result *); const struct kitty_parse_result *);
int image_get_fallback_at(struct tty *, struct screen *, u_int, u_int, int image_grid_get_at(struct grid *, u_int, u_int, struct image **,
const struct grid_cell *, struct grid_cell *); u_int *, u_int *);
const struct grid_cell *image_map_cell(struct tty *, struct grid *, u_int,
u_int, const struct grid_cell *, struct grid_cell *, int *);
struct image *image_rect_get_image(const struct image_rect *); struct image *image_rect_get_image(const struct image_rect *);
void image_rect_get_coords(const struct image_rect *, u_int *, u_int *, void image_rect_get_coords(const struct image_rect *, u_int *, u_int *,
u_int *, u_int *, u_int *, u_int *); u_int *, u_int *, u_int *, u_int *);
@@ -211,15 +191,16 @@ void image_draw_line(struct tty *, struct screen *, u_int, u_int, u_int,
void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int); void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
void image_redraw_finish(struct tty *); void image_redraw_finish(struct tty *);
void image_draw_flush(struct tty *); void image_draw_flush(struct tty *);
int image_backend_flags(struct tty *); int image_backend_get_flags(struct tty *);
int image_tty_update(struct tty *); int image_tty_update(struct tty *);
void image_tty_geometry_changed(struct tty *); void image_tty_size_changed(struct tty *);
void image_tty_free(struct tty *, int); void image_tty_free(struct tty *, int);
/* image-fallback.c */ /* image-fallback.c */
void fallback_get_cell(struct tty *, struct image *, u_int, u_int, int fallback_select(struct tty *);
const struct grid_cell *, struct grid_cell *); int fallback_get_flags(struct tty *);
void fallback_free(struct image *); const struct grid_cell *fallback_map_cell(struct tty *, struct grid *, u_int,
u_int, const struct grid_cell *, struct grid_cell *, int *);
/* image-kitty.c */ /* image-kitty.c */
struct image *kitty_parse_image(void **, const u_char *, size_t, u_int, struct image *kitty_parse_image(void **, const u_char *, size_t, u_int,
@@ -230,6 +211,8 @@ int kitty_cell_is_placeholder(const struct grid_cell *);
void kitty_delete_images(void *, struct screen_write_ctx *, void kitty_delete_images(void *, struct screen_write_ctx *,
const struct kitty_parse_result *); const struct kitty_parse_result *);
void kitty_free_state(void *); void kitty_free_state(void *);
int kitty_select(struct tty *);
int kitty_get_flags(struct tty *);
void kitty_draw_rect(struct tty *, const struct image_rect *); void kitty_draw_rect(struct tty *, const struct image_rect *);
void kitty_redraw_start(struct tty *, u_int, u_int, u_int, u_int); void kitty_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
void kitty_redraw_finish(struct tty *); void kitty_redraw_finish(struct tty *);
@@ -237,6 +220,8 @@ void kitty_free_output_state(struct tty *, int);
/* image-sixel.c */ /* image-sixel.c */
#define SIXEL_COLOUR_REGISTERS 1024 #define SIXEL_COLOUR_REGISTERS 1024
int sixel_select(struct tty *);
int sixel_get_flags(struct tty *);
struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int, struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int,
u_int); u_int);
void sixel_write(struct screen_write_ctx *, struct sixel_image *, void sixel_write(struct screen_write_ctx *, struct sixel_image *,

8
tmux.1
View File

@@ -5194,12 +5194,8 @@ Supports extended keys.
Supports focus reporting. Supports focus reporting.
.It hyperlinks .It hyperlinks
Supports OSC 8 hyperlinks. Supports OSC 8 hyperlinks.
.It image-quadrants
Allows image fallbacks to use Unicode quadrant block characters.
.It image-sextants
Allows image fallbacks to use Unicode sextant block characters.
.It imagescroll .It imagescroll
Moves image content along with scrolling regions. Moves image content with scroll regions.
.It ignorefkeys .It ignorefkeys
Ignore function keys from Ignore function keys from
.Xr terminfo 5 .Xr terminfo 5
@@ -8990,6 +8986,8 @@ These are set automatically if the
capability is present. capability is present.
.It Em \&Hls .It Em \&Hls
Set or clear a hyperlink annotation. Set or clear a hyperlink annotation.
.It Em \&Iscrl
Indicates that the terminal moves image content with scroll regions.
.It Em \&Nobr .It Em \&Nobr
Tell Tell
.Nm .Nm

38
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1459 2026/10/13 10:21:58 nicm Exp $ */ /* $OpenBSD: tmux.h,v 1.1461 2026/10/14 10:34:14 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -499,6 +499,7 @@ enum tty_code_code {
TTYC_IND, TTYC_IND,
TTYC_INDN, TTYC_INDN,
TTYC_INVIS, TTYC_INVIS,
TTYC_ISCRL,
TTYC_KCBT, TTYC_KCBT,
TTYC_KCUB1, TTYC_KCUB1,
TTYC_KCUD1, TTYC_KCUD1,
@@ -1478,10 +1479,8 @@ struct window {
struct redraw_damages damage; struct redraw_damages damage;
u_int damage_count; u_int damage_count;
#ifdef ENABLE_IMAGES uint64_t scroll_generation;
uint64_t image_scroll_epoch; int scroll_pending;
int image_scroll_pending;
#endif
struct menu_data *menu; struct menu_data *menu;
u_int menu_last_px; u_int menu_last_px;
@@ -1752,11 +1751,6 @@ struct tty_term {
#define TERM_INVALIDMS 0x80 #define TERM_INVALIDMS 0x80
#define TERM_NOREPLACE 0x100 #define TERM_NOREPLACE 0x100
#define TERM_KITTYGRAPHICS 0x200 #define TERM_KITTYGRAPHICS 0x200
#ifdef ENABLE_IMAGES
#define TERM_IMAGE_QUADRANTS 0x400
#define TERM_IMAGE_SEXTANTS 0x800
#define TERM_IMAGESCROLL 0x1000
#endif
int flags; int flags;
LIST_ENTRY(tty_term) entry; LIST_ENTRY(tty_term) entry;
@@ -1861,12 +1855,14 @@ struct tty {
struct event key_timer; struct event key_timer;
struct tty_key *key_tree; struct tty_key *key_tree;
struct window *scroll_window;
uint64_t scroll_generation;
int scroll_failed;
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
const struct image_backend *image_backend; struct image_backend *image_backend;
void *image_data; void *image_data;
struct window *image_scroll_window;
uint64_t image_scroll_epoch;
int image_scroll_failed;
#endif #endif
}; };
@@ -2363,7 +2359,7 @@ struct client {
int references; int references;
int theme_colours[COLOUR_THEME_COUNT]; int theme_colours[COLOUR_THEME_COUNT];
void *pan_window; void *pan_window;
u_int pan_ox; u_int pan_ox;
@@ -2957,7 +2953,7 @@ void tty_cell(struct tty *, const struct grid_cell *,
const struct tty_style_ctx *); const struct tty_style_ctx *);
int tty_init(struct tty *, struct client *); int tty_init(struct tty *, struct client *);
void tty_resize(struct tty *); void tty_resize(struct tty *);
void tty_set_size(struct tty *, u_int, u_int, u_int, u_int); int tty_set_size(struct tty *, u_int, u_int, u_int, u_int);
void tty_invalidate(struct tty *); void tty_invalidate(struct tty *);
void tty_start_tty(struct tty *); void tty_start_tty(struct tty *);
void tty_send_requests(struct tty *); void tty_send_requests(struct tty *);
@@ -3042,7 +3038,6 @@ void tty_default_features(struct client *, const char *, u_int);
/* tty-acs.c */ /* tty-acs.c */
int tty_acs_needed(struct tty *); int tty_acs_needed(struct tty *);
const char *tty_acs_get(struct tty *, u_char); const char *tty_acs_get(struct tty *, u_char);
u_char tty_acs_image_sextant(u_int);
int tty_acs_reverse_get(struct tty *, const char *, size_t); int tty_acs_reverse_get(struct tty *, const char *, size_t);
const struct utf8_data *tty_acs_double_borders(int); const struct utf8_data *tty_acs_double_borders(int);
const struct utf8_data *tty_acs_heavy_borders(int); const struct utf8_data *tty_acs_heavy_borders(int);
@@ -3656,11 +3651,8 @@ int redraw_client_has_window(struct client *, struct window *);
void redraw_invalidate_scene(struct window *); void redraw_invalidate_scene(struct window *);
void redraw_invalidate_all_scenes(void); void redraw_invalidate_all_scenes(void);
void redraw_damage_window(struct window *, u_int, u_int, u_int, u_int); void redraw_damage_window(struct window *, u_int, u_int, u_int, u_int);
#ifdef ENABLE_IMAGES void redraw_damage_scroll(struct window *, u_int, u_int, u_int, u_int);
void redraw_damage_window_scroll(struct window *, u_int, u_int, u_int, void redraw_scroll_result(struct tty *, const struct tty_ctx *, int);
u_int);
void redraw_image_scroll_result(struct tty *, const struct tty_ctx *, int);
#endif
void redraw_free_damage(struct window *); void redraw_free_damage(struct window *);
void redraw_client_damage(struct client *); void redraw_client_damage(struct client *);
int redraw_get_status_border_cell_type(struct redraw_span **, u_int); int redraw_get_status_border_cell_type(struct redraw_span **, u_int);

View File

@@ -64,67 +64,9 @@ static const struct tty_acs_entry tty_acs_table[] = {
{ '{', "\317\200" }, /* greek pi */ { '{', "\317\200" }, /* greek pi */
{ '|', "\342\211\240" }, /* not-equal */ { '|', "\342\211\240" }, /* not-equal */
{ '}', "\302\243" }, /* UK pound sign */ { '}', "\302\243" }, /* UK pound sign */
{ '~', "\302\267" }, /* bullet */ { '~', "\302\267" } /* bullet */
#ifdef ENABLE_IMAGES
{ TTY_ACS_IMAGE_SHADE_LIGHT, "\342\226\221" },
{ TTY_ACS_IMAGE_SHADE_MEDIUM, "\342\226\222" },
{ TTY_ACS_IMAGE_SHADE_DARK, "\342\226\223" },
{ TTY_ACS_IMAGE_BLOCK, "\342\226\210" },
{ TTY_ACS_IMAGE_HALF_UPPER, "\342\226\200" },
{ TTY_ACS_IMAGE_HALF_LOWER, "\342\226\204" },
{ TTY_ACS_IMAGE_HALF_LEFT, "\342\226\214" },
{ TTY_ACS_IMAGE_HALF_RIGHT, "\342\226\220" },
{ TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, "\342\226\226" },
{ TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT, "\342\226\227" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT, "\342\226\230" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT,
"\342\226\231" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT, "\342\226\232" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT,
"\342\226\233" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT,
"\342\226\234" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, "\342\226\235" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT, "\342\226\236" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT,
"\342\226\237" }
#endif
}; };
#ifdef ENABLE_IMAGES
static char tty_acs_image_sextants[60][5];
/* Encode the Unicode sextant characters used by the image fallback. */
static void
tty_acs_image_sextants_init(void)
{
u_int cp, i;
if (tty_acs_image_sextants[0][0] != '\0')
return;
for (i = 0; i < nitems(tty_acs_image_sextants); i++) {
cp = 0x1fb00 + i;
tty_acs_image_sextants[i][0] = 0xf0 | (cp >> 18);
tty_acs_image_sextants[i][1] = 0x80 | ((cp >> 12) & 0x3f);
tty_acs_image_sextants[i][2] = 0x80 | ((cp >> 6) & 0x3f);
tty_acs_image_sextants[i][3] = 0x80 | (cp & 0x3f);
}
}
/* Return the sextant ACS key for a six-bit foreground mask. */
u_char
tty_acs_image_sextant(u_int mask)
{
if (mask == 0 || mask == 21 || mask == 42 || mask >= 63)
return (0);
if (mask < 21)
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 1);
if (mask < 42)
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 2);
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 3);
}
#endif
/* Table mapping UTF-8 to ACS entries. */ /* Table mapping UTF-8 to ACS entries. */
struct tty_acs_reverse_entry { struct tty_acs_reverse_entry {
const char *string; const char *string;
@@ -297,15 +239,6 @@ tty_acs_get(struct tty *tty, u_char ch)
return (&tty->term->acs[ch][0]); return (&tty->term->acs[ch][0]);
} }
#ifdef ENABLE_IMAGES
if (ch >= TTY_ACS_IMAGE_SEXTANT_FIRST &&
ch <= TTY_ACS_IMAGE_SEXTANT_LAST) {
tty_acs_image_sextants_init();
return (tty_acs_image_sextants[
ch - TTY_ACS_IMAGE_SEXTANT_FIRST]);
}
#endif
/* Otherwise look up the UTF-8 translation. */ /* Otherwise look up the UTF-8 translation. */
entry = bsearch(&ch, tty_acs_table, nitems(tty_acs_table), entry = bsearch(&ch, tty_acs_table, nitems(tty_acs_table),
sizeof tty_acs_table[0], tty_acs_cmp); sizeof tty_acs_table[0], tty_acs_cmp);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-draw.c,v 1.18 2026/10/12 15:28:45 nicm Exp $ */ /* $OpenBSD: tty-draw.c,v 1.19 2026/10/13 22:44:53 nicm Exp $ */
/* /*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -122,11 +122,6 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
struct grid *gd = s->grid; struct grid *gd = s->grid;
const struct grid_cell *gcp; const struct grid_cell *gcp;
struct grid_cell gc, ngc, last; struct grid_cell gc, ngc, last;
#ifdef ENABLE_IMAGES
struct grid_cell image_gc;
struct kitty_placeholder placeholder;
int image_status;
#endif
struct grid_line *gl; struct grid_line *gl;
u_int i, j, last_i, cx, ex, width; u_int i, j, last_i, cx, ex, width;
u_int cellsize, bg; u_int cellsize, bg;
@@ -136,6 +131,11 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
enum tty_draw_line_state current_state, next_state; enum tty_draw_line_state current_state, next_state;
struct tty_style_ctx default_style_ctx = { 0 }; struct tty_style_ctx default_style_ctx = { 0 };
const struct grid_cell *defaults; const struct grid_cell *defaults;
#ifdef ENABLE_IMAGES
struct image *im;
struct kitty_placeholder placeholder;
u_int x, y;
#endif
if (style_ctx == NULL) { if (style_ctx == NULL) {
default_style_ctx.defaults = &grid_default_cell; default_style_ctx.defaults = &grid_default_cell;
@@ -193,6 +193,10 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
grid_view_get_cell(gd, i, py, &gc); grid_view_get_cell(gd, i, py, &gc);
if (~gc.flags & GRID_FLAG_PADDING) if (~gc.flags & GRID_FLAG_PADDING)
break; break;
#ifdef ENABLE_IMAGES
if (image_grid_get_at(gd, i, gd->hsize + py, &im, &x, &y) == 1)
break;
#endif
cx++; cx++;
} }
if (cx != 0) { if (cx != 0) {
@@ -256,18 +260,14 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
if (px >= ex || i >= ex - px) { if (px >= ex || i >= ex - px) {
/* Outside the area being drawn. */ /* Outside the area being drawn. */
empty = nx - i;
gcp = &grid_default_cell; gcp = &grid_default_cell;
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
image_status = image_get_fallback_at(tty, s, px + i, /* Map an image cell if needed. */
py, gcp, &image_gc); gcp = image_map_cell(tty, gd, px + i,
if (image_status == 1) { gd->hsize + py, gcp, &gc, &empty);
gcp = &image_gc;
empty = 0;
} else if (image_status == -1)
empty = 1;
else
#endif #endif
empty = nx - i;
/* Flush the background before clearing. */ /* Flush the background before clearing. */
if (empty != 0 && gcp->bg != last.bg) if (empty != 0 && gcp->bg != last.bg)
@@ -283,15 +283,16 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
} }
#endif #endif
gcp = &gc; gcp = &gc;
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
if (image_get_fallback_at(tty, s, px + i, py, &gc, /* Map an image cell if needed. */
&image_gc) == 1) { gcp = image_map_cell(tty, gd, px + i,
gcp = &image_gc; gd->hsize + py, gcp, &gc, NULL);
}
#endif #endif
/* Work out empty cells. */ /* Work out empty cells. */
empty = tty_draw_line_get_empty(gcp, &last, nx - i); empty = tty_draw_line_get_empty(gcp, &last,
nx - i);
if (empty == 0) { if (empty == 0) {
/* Update for codeset if needed. */ /* Update for codeset if needed. */
gcp = tty_check_codeset(tty, gcp); gcp = tty_check_codeset(tty, gcp);

View File

@@ -376,31 +376,16 @@ static const struct tty_feature tty_feature_kittygraphics = {
TERM_KITTYGRAPHICS TERM_KITTYGRAPHICS
}; };
#ifdef ENABLE_IMAGES /* Terminal moves images with scroll regions. */
static const char *const tty_feature_image_capabilities[] = { static const char *const tty_feature_imagescroll_capabilities[] = {
"Iscrl",
NULL NULL
}; };
static const struct tty_feature tty_feature_image_quadrants = {
"image-quadrants",
tty_feature_image_capabilities,
TERM_IMAGE_QUADRANTS
};
static const struct tty_feature tty_feature_image_sextants = {
"image-sextants",
tty_feature_image_capabilities,
TERM_IMAGE_SEXTANTS
};
/*
* Terminal moves images with a scrolling region. Text margin support does
* not imply this capability.
*/
static const struct tty_feature tty_feature_imagescroll = { static const struct tty_feature tty_feature_imagescroll = {
"imagescroll", "imagescroll",
tty_feature_image_capabilities, tty_feature_imagescroll_capabilities,
TERM_IMAGESCROLL 0
}; };
#endif
/* Terminal supports the OSC 9;4 progress bar. */ /* Terminal supports the OSC 9;4 progress bar. */
static const char *const tty_feature_progressbar_capabilities[] = { static const char *const tty_feature_progressbar_capabilities[] = {
@@ -432,12 +417,8 @@ static const struct tty_feature *const tty_features[] = {
&tty_feature_extkeys, &tty_feature_extkeys,
&tty_feature_focus, &tty_feature_focus,
&tty_feature_ignorefkeys, &tty_feature_ignorefkeys,
&tty_feature_kittygraphics,
#ifdef ENABLE_IMAGES
&tty_feature_image_quadrants,
&tty_feature_image_sextants,
&tty_feature_imagescroll, &tty_feature_imagescroll,
#endif &tty_feature_kittygraphics,
&tty_feature_margins, &tty_feature_margins,
&tty_feature_mouse, &tty_feature_mouse,
&tty_feature_osc7, &tty_feature_osc7,

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-keys.c,v 1.217 2026/10/12 12:26:46 nicm Exp $ */ /* $OpenBSD: tty-keys.c,v 1.218 2026/10/14 08:33:19 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1914,7 +1914,8 @@ tty_keys_winsz(struct tty *tty, const char *buf, size_t len, size_t *size,
return (0); return (0);
/* Window size in characters. */ /* Window size in characters. */
tty_set_size(tty, sx, sy, tty->xpixel, tty->ypixel); if (tty_set_size(tty, sx, sy, tty->xpixel, tty->ypixel))
server_redraw_client(c);
return (0); return (0);
} else if (sscanf(tmp, "4;%u;%u", &ypixel, &xpixel) == 2) { } else if (sscanf(tmp, "4;%u;%u", &ypixel, &xpixel) == 2) {
*size = end + 1; *size = end + 1;
@@ -1924,10 +1925,9 @@ tty_keys_winsz(struct tty *tty, const char *buf, size_t len, size_t *size,
/* Window size in pixels. */ /* Window size in pixels. */
char_x = (xpixel && tty->sx) ? xpixel / tty->sx : 0; char_x = (xpixel && tty->sx) ? xpixel / tty->sx : 0;
char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0; char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0;
tty_set_size(tty, tty->sx, tty->sy, char_x, char_y); if (tty_set_size(tty, tty->sx, tty->sy, char_x, char_y))
server_redraw_client(c);
tty_invalidate(tty); tty_invalidate(tty);
/* Windows take their cell size from the clients. */
recalculate_sizes(); recalculate_sizes();
tty->flags &= ~TTY_WINSIZEQUERY; tty->flags &= ~TTY_WINSIZEQUERY;

View File

@@ -116,6 +116,7 @@ static const struct tty_term_code_entry tty_term_codes[] = {
[TTYC_IND] = { TTYCODE_STRING, "ind" }, [TTYC_IND] = { TTYCODE_STRING, "ind" },
[TTYC_INDN] = { TTYCODE_STRING, "indn" }, [TTYC_INDN] = { TTYCODE_STRING, "indn" },
[TTYC_INVIS] = { TTYCODE_STRING, "invis" }, [TTYC_INVIS] = { TTYCODE_STRING, "invis" },
[TTYC_ISCRL] = { TTYCODE_FLAG, "Iscrl" },
[TTYC_KCBT] = { TTYCODE_STRING, "kcbt" }, [TTYC_KCBT] = { TTYCODE_STRING, "kcbt" },
[TTYC_KCUB1] = { TTYCODE_STRING, "kcub1" }, [TTYC_KCUB1] = { TTYCODE_STRING, "kcub1" },
[TTYC_KCUD1] = { TTYCODE_STRING, "kcud1" }, [TTYC_KCUD1] = { TTYCODE_STRING, "kcud1" },

71
tty.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty.c,v 1.486 2026/10/12 10:39:05 nicm Exp $ */ /* $OpenBSD: tty.c,v 1.487 2026/10/14 08:33:19 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -155,25 +155,22 @@ tty_resize(struct tty *tty)
tty_invalidate(tty); tty_invalidate(tty);
} }
void int
tty_set_size(struct tty *tty, u_int sx, u_int sy, u_int xpixel, u_int ypixel) tty_set_size(struct tty *tty, u_int sx, u_int sy, u_int xpixel, u_int ypixel)
{ {
#ifdef ENABLE_IMAGES if (tty->sx == sx &&
int geometry_changed = 0; tty->sy == sy &&
tty->xpixel == xpixel &&
if (tty->xpixel != xpixel || tty->ypixel != ypixel) tty->ypixel == ypixel)
geometry_changed = 1; return (0);
#endif
tty->sx = sx; tty->sx = sx;
tty->sy = sy; tty->sy = sy;
tty->xpixel = xpixel; tty->xpixel = xpixel;
tty->ypixel = ypixel; tty->ypixel = ypixel;
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
if (geometry_changed) { image_tty_size_changed(tty);
image_tty_geometry_changed(tty);
server_redraw_client(tty->client);
}
#endif #endif
return (1);
} }
static void static void
@@ -586,16 +583,10 @@ tty_update_features(struct tty *tty)
tty_puts(tty, tty_term_string(tty->term, TTYC_ENFCS)); tty_puts(tty, tty_term_string(tty->term, TTYC_ENFCS));
tty_puts(tty, tty_term_string(tty->term, TTYC_ENESC)); tty_puts(tty, tty_term_string(tty->term, TTYC_ENESC));
/* if (changed) {
* Redraw only for changed features; repeated DA replies must not erase server_redraw_client(c);
* images sent through DCS passthrough. tty_invalidate(tty);
*/ }
if (!changed)
return;
server_redraw_client(c);
tty_invalidate(tty);
} }
void void
@@ -1695,17 +1686,12 @@ tty_cmd_reverseindex(struct tty *tty, const struct tty_ctx *ctx)
!tty_term_has(tty->term, TTYC_RIN)) || !tty_term_has(tty->term, TTYC_RIN)) ||
ctx->sx == 1 || ctx->sx == 1 ||
ctx->sy == 1) { ctx->sy == 1) {
#ifdef ENABLE_IMAGES redraw_scroll_result(tty, ctx, 1);
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx); tty_redraw_region(tty, ctx);
return; return;
} }
#ifdef ENABLE_IMAGES redraw_scroll_result(tty, ctx, 0);
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx); tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -1730,17 +1716,12 @@ tty_cmd_linefeed(struct tty *tty, const struct tty_ctx *ctx)
!tty_term_has(tty->term, TTYC_CSR) || !tty_term_has(tty->term, TTYC_CSR) ||
ctx->sx == 1 || ctx->sx == 1 ||
ctx->sy == 1) { ctx->sy == 1) {
#ifdef ENABLE_IMAGES redraw_scroll_result(tty, ctx, 1);
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx); tty_redraw_region(tty, ctx);
return; return;
} }
#ifdef ENABLE_IMAGES redraw_scroll_result(tty, ctx, 0);
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx); tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -1775,17 +1756,12 @@ tty_cmd_scrollup(struct tty *tty, const struct tty_ctx *ctx)
!tty_term_has(tty->term, TTYC_CSR) || !tty_term_has(tty->term, TTYC_CSR) ||
ctx->sx == 1 || ctx->sx == 1 ||
ctx->sy == 1) { ctx->sy == 1) {
#ifdef ENABLE_IMAGES redraw_scroll_result(tty, ctx, 1);
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx); tty_redraw_region(tty, ctx);
return; return;
} }
#ifdef ENABLE_IMAGES redraw_scroll_result(tty, ctx, 0);
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx); tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -1820,17 +1796,12 @@ tty_cmd_scrolldown(struct tty *tty, const struct tty_ctx *ctx)
!tty_term_has(tty->term, TTYC_RIN)) || !tty_term_has(tty->term, TTYC_RIN)) ||
ctx->sx == 1 || ctx->sx == 1 ||
ctx->sy == 1) { ctx->sy == 1) {
#ifdef ENABLE_IMAGES redraw_scroll_result(tty, ctx, 1);
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx); tty_redraw_region(tty, ctx);
return; return;
} }
#ifdef ENABLE_IMAGES redraw_scroll_result(tty, ctx, 0);
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx); tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);

View File

@@ -89,19 +89,20 @@ static const char *window_client_info_lines[] = {
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(hyperlinks) " " WINDOW_CLIENT_FEATURE(hyperlinks) " "
WINDOW_CLIENT_FEATURE(ignorefkeys) " " WINDOW_CLIENT_FEATURE(ignorefkeys) " "
WINDOW_CLIENT_FEATURE(kittygraphics) " " WINDOW_CLIENT_FEATURE(imagescroll) " "
WINDOW_CLIENT_FEATURE(margins), WINDOW_CLIENT_FEATURE(kittygraphics),
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(margins) " "
WINDOW_CLIENT_FEATURE(mouse) " " WINDOW_CLIENT_FEATURE(mouse) " "
WINDOW_CLIENT_FEATURE(osc7) " " WINDOW_CLIENT_FEATURE(osc7) " "
WINDOW_CLIENT_FEATURE(overline) " " WINDOW_CLIENT_FEATURE(overline),
WINDOW_CLIENT_FEATURE(progressbar),
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(progressbar) " "
WINDOW_CLIENT_FEATURE(rectfill) " " WINDOW_CLIENT_FEATURE(rectfill) " "
WINDOW_CLIENT_FEATURE(sixel) " " WINDOW_CLIENT_FEATURE(sixel) " "
WINDOW_CLIENT_FEATURE(strikethrough) " " WINDOW_CLIENT_FEATURE(strikethrough),
WINDOW_CLIENT_FEATURE(sync),
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(sync) " "
WINDOW_CLIENT_FEATURE(title) " " WINDOW_CLIENT_FEATURE(title) " "
WINDOW_CLIENT_FEATURE(usstyle) " " WINDOW_CLIENT_FEATURE(usstyle) " "
WINDOW_CLIENT_FEATURE(utf8), WINDOW_CLIENT_FEATURE(utf8),

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-copy.c,v 1.437 2026/10/08 07:50:05 nicm Exp $ */ /* $OpenBSD: window-copy.c,v 1.438 2026/10/13 23:17:42 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -297,9 +297,11 @@ struct window_copy_mode_data {
u_int oy; /* number of lines scrolled up */ u_int oy; /* number of lines scrolled up */
u_int image_base; /* hsize - oy images were last drawn for */ #ifdef ENABLE_IMAGES
int image_base_set; /* image_base is valid */ u_int image_base; /* where images were last drawn for */
int image_refresh; /* current redraw needs image refresh */ int image_base_set;
int image_refresh;
#endif
u_int selx; /* beginning of selection */ u_int selx; /* beginning of selection */
u_int sely; u_int sely;
@@ -545,7 +547,8 @@ window_copy_sync_backing(struct window_mode_entry *wme)
if (collected > 0) { if (collected > 0) {
grid_free_lines(dg, 0, collected); grid_free_lines(dg, 0, collected);
memmove(&dg->linedata[0], &dg->linedata[collected], memmove(&dg->linedata[0], &dg->linedata[collected],
(old_hsize + sy - collected) * sizeof *dg->linedata); (old_hsize + sy - collected) *
sizeof *dg->linedata);
memset(&dg->linedata[old_hsize + sy - collected], 0, memset(&dg->linedata[old_hsize + sy - collected], 0,
collected * sizeof *dg->linedata); collected * sizeof *dg->linedata);
} }
@@ -564,7 +567,7 @@ window_copy_sync_backing(struct window_mode_entry *wme)
*/ */
dg->hsize = new_hsize; dg->hsize = new_hsize;
/* Copy the newly scrolled history, then refresh the viewport. */ /* Copy the newly scrolled history, then refresh the view. */
if (added > 0) if (added > 0)
grid_duplicate_lines(dg, kept, sg, kept, added); grid_duplicate_lines(dg, kept, sg, kept, added);
grid_duplicate_lines(dg, new_hsize, sg, new_hsize, sy); grid_duplicate_lines(dg, new_hsize, sg, new_hsize, sy);
@@ -2328,6 +2331,7 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs)
{ {
struct window_mode_entry *wme = cs->wme; struct window_mode_entry *wme = cs->wme;
struct options *so = cs->s->options; struct options *so = cs->s->options;
struct options *oo = wme->wp->window->options;
struct window_copy_mode_data *data = wme->data; struct window_copy_mode_data *data = wme->data;
struct grid_reader gr; struct grid_reader gr;
const char *s = args_string(cs->wargs, 0); const char *s = args_string(cs->wargs, 0);
@@ -2347,7 +2351,8 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs)
/* /*
* Set up the reset positions normally initialized by select-word or * Set up the reset positions normally initialized by select-word or
* select-line, including when changing the mode of an existing selection. * select-line, including when changing the mode of an existing
* selection.
*/ */
if (data->cursordrag == CURSORDRAG_SEL) { if (data->cursordrag == CURSORDRAG_SEL) {
fx = data->endselx; fx = data->endselx;
@@ -2374,8 +2379,7 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs)
grid_reader_start(&gr, data->backing->grid, ex, ey); grid_reader_start(&gr, data->backing->grid, ex, ey);
if (data->selflag == SEL_WORD) { if (data->selflag == SEL_WORD) {
grid_reader_cursor_next_word_end(&gr, data->separators); grid_reader_cursor_next_word_end(&gr, data->separators);
if (options_get_number(wme->wp->window->options, "mode-keys") == if (options_get_number(oo, "mode-keys") == MODEKEY_VI)
MODEKEY_VI)
grid_reader_cursor_left(&gr, 1); grid_reader_cursor_left(&gr, 1);
} else } else
grid_reader_cursor_end_of_line(&gr, 1, 0); grid_reader_cursor_end_of_line(&gr, 1, 0);
@@ -5591,9 +5595,8 @@ window_copy_write_one(struct window_mode_entry *wme,
grid_get_cell(gd, fx, fy, &gc); grid_get_cell(gd, fx, fy, &gc);
if (fx + gc.data.width <= nx) { if (fx + gc.data.width <= nx) {
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
/* Write image cells without styling or damage. */ if (image_grid_area_has_images(gd, fx, fy,
if (image_grid_area_has_images(gd, fx, fy, gc.data.width, gc.data.width, 1)) {
1)) {
grid_view_set_cell(ctx->s->grid, px + fx, py, grid_view_set_cell(ctx->s->grid, px + fx, py,
&gc); &gc);
screen_write_cursormove(ctx, screen_write_cursormove(ctx,
@@ -5606,8 +5609,8 @@ window_copy_write_one(struct window_mode_entry *wme,
if (gc.flags & GRID_FLAG_PADDING) { if (gc.flags & GRID_FLAG_PADDING) {
if (ctx->s->cy == py && ctx->s->cx <= px + fx) { if (ctx->s->cy == py && ctx->s->cx <= px + fx) {
gc.flags &= ~GRID_FLAG_PADDING; gc.flags &= ~GRID_FLAG_PADDING;
screen_write_cursormove(ctx, px + fx, py, screen_write_cursormove(ctx, px + fx,
0); py, 0);
screen_write_putc(ctx, &gc, ' '); screen_write_putc(ctx, &gc, ' ');
} }
} else if (gc.flags & GRID_FLAG_TAB) { } else if (gc.flags & GRID_FLAG_TAB) {
@@ -5796,6 +5799,9 @@ window_copy_write_line(struct window_mode_entry *wme,
char *expanded; char *expanded;
struct format_tree *ft; struct format_tree *ft;
int current, mode; int current, mode;
#ifdef ENABLE_IMAGES
int flags;
#endif
width = window_copy_line_number_width(wme); width = window_copy_line_number_width(wme);
if (width >= sx) if (width >= sx)
@@ -5850,13 +5856,13 @@ window_copy_write_line(struct window_mode_entry *wme,
content_sx, &mgc, &cgc, &mkgc, &clgc); content_sx, &mgc, &cgc, &mkgc, &clgc);
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
/* Copy image spans; refresh only when the history view moves. */ image_grid_free_line(s->grid, &s->grid->linedata[s->grid->hsize + py]);
image_grid_free_line(s->grid,
&s->grid->linedata[s->grid->hsize + py]);
image_grid_copy_area(s->grid, width, s->grid->hsize + py, image_grid_copy_area(s->grid, width, s->grid->hsize + py,
data->backing->grid, 0, hsize - data->oy + py, content_sx, 1); data->backing->grid, 0, hsize - data->oy + py, content_sx, 1);
if (data->image_refresh) if (data->image_refresh)
image_redraw_area(ctx, width, py, content_sx, 1); image_redraw_area(ctx, width, py, content_sx, 1);
flags = ctx->flags;
ctx->flags |= SCREEN_WRITE_INPUT;
#endif #endif
if (py == 0 && s->rupper < s->rlower && !data->hide_position) { if (py == 0 && s->rupper < s->rlower && !data->hide_position) {
@@ -5878,6 +5884,9 @@ window_copy_write_line(struct window_mode_entry *wme,
screen_write_putc(ctx, &grid_default_cell, '$'); screen_write_putc(ctx, &grid_default_cell, '$');
} }
#ifdef ENABLE_IMAGES
ctx->flags = flags;
#endif
format_free(ft); format_free(ft);
} }
@@ -5910,32 +5919,30 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y)
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
/* Refresh images only when the visible history range has moved. */ /* Refresh images when the visible range of history changes. */
static void static void
window_copy_update_image_refresh(struct window_copy_mode_data *data) window_copy_update_image_refresh(struct window_copy_mode_data *data)
{ {
u_int base; u_int base = screen_hsize(data->backing) - data->oy;
base = screen_hsize(data->backing) - data->oy;
data->image_refresh = 0;
if (!data->image_base_set || base != data->image_base) if (!data->image_base_set || base != data->image_base)
data->image_refresh = 1; data->image_refresh = 1;
else
data->image_refresh = 0;
data->image_base = base; data->image_base = base;
data->image_base_set = 1; data->image_base_set = 1;
} }
/* Return whether the visible backing range contains image spans. */ /* Check whether visible history contains images. */
static int static int
window_copy_visible_has_images(struct window_copy_mode_data *data) window_copy_visible_has_images(struct window_copy_mode_data *data)
{ {
struct grid *gd = data->backing->grid; struct grid *gd = data->backing->grid;
u_int sy = screen_size_y(&data->screen); struct screen *s = &data->screen;
int has_images; u_int sx = screen_size_x(s), sy = screen_size_y(s);
u_int hsize = screen_hsize(data->backing);
has_images = image_grid_area_has_images(gd, 0, return (image_grid_area_has_images(gd, 0, hsize - data->oy, sx, sy));
screen_hsize(data->backing) - data->oy,
screen_size_x(&data->screen), sy);
return (has_images);
} }
#endif #endif
@@ -5946,7 +5953,7 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny)
struct window_copy_mode_data *data = wme->data; struct window_copy_mode_data *data = wme->data;
struct screen *s = &data->screen; struct screen *s = &data->screen;
struct screen_write_ctx ctx; struct screen_write_ctx ctx;
u_int i; u_int i, off;
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
window_copy_update_image_refresh(data); window_copy_update_image_refresh(data);
@@ -5970,9 +5977,8 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny)
screen_write_start_pane(&ctx, wp, NULL); screen_write_start_pane(&ctx, wp, NULL);
for (i = py; i < py + ny; i++) for (i = py; i < py + ny; i++)
window_copy_write_line(wme, &ctx, i); window_copy_write_line(wme, &ctx, i);
screen_write_cursormove(&ctx, off = window_copy_cursor_offset(wme, data->cx, screen_size_x(s));
window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, screen_write_cursormove(&ctx, off, data->cy, 0);
0);
screen_write_stop(&ctx); screen_write_stop(&ctx);
window_pane_scrollbar_redraw(wp); window_pane_scrollbar_redraw(wp);