mirror of
https://github.com/tmux/tmux.git
synced 2026-10-15 07:44:24 +00:00
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:
2
.github/workflows/regress.yml
vendored
2
.github/workflows/regress.yml
vendored
@@ -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
|
||||||
|
|||||||
569
image-fallback.c
569
image-fallback.c
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
445
image-grid.c
445
image-grid.c
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[] = {
|
||||||
|
|||||||
977
image-sixel.c
977
image-sixel.c
File diff suppressed because it is too large
Load Diff
205
image.c
205
image.c
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}'
|
||||||
|
|||||||
@@ -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
10
regress/image-common.inc
Normal 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
108
regress/image-fallback.sh
Executable 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
34
regress/image-feature-redraw.sh
Executable 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
|
||||||
@@ -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
125
regress/image-grid-operations.sh
Executable 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
76
regress/image-grid.sh
Executable 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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}')
|
||||||
|
|||||||
@@ -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
74
regress/image-lifecycle.sh
Executable 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
96
regress/image-sixel-output.sh
Executable 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
|
||||||
@@ -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
45
regress/image-sixel-palette.sh
Executable 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
68
regress/image-sixel-parse.sh
Executable 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
|
||||||
@@ -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
|
|
||||||
|
|||||||
54
regress/image-sixel-transparency.sh
Executable file
54
regress/image-sixel-transparency.sh
Executable 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
133
regress/image_scroll.py
Normal 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
192
regress/image_terminal.py
Normal 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
307
regress/image_test.py
Normal 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)
|
||||||
@@ -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
|
||||||
|
|||||||
251
screen-redraw.c
251
screen-redraw.c
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
87
tmux-image.h
87
tmux-image.h
@@ -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
8
tmux.1
@@ -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
38
tmux.h
@@ -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);
|
||||||
|
|||||||
69
tty-acs.c
69
tty-acs.c
@@ -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);
|
||||||
|
|||||||
41
tty-draw.c
41
tty-draw.c
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
10
tty-keys.c
10
tty-keys.c
@@ -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;
|
||||||
|
|||||||
@@ -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
71
tty.c
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user