From e9fffcc7e94f57116e198225a5c051bef24a0bc4 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 19:20:42 +0100 Subject: [PATCH 01/12] Image grid infrastructure. --- Makefile.am | 3 +- grid.c | 36 ++++- image-grid.c | 382 +++++++++++++++++++++++++++++++++++++++++++ image.c | 422 ++++++++++++++++++++++++++++-------------------- input.c | 19 --- screen-redraw.c | 9 -- screen-write.c | 152 ----------------- screen.c | 50 ------ tmux-image.h | 99 ++++++++++++ tmux.h | 70 ++------ tty.c | 124 -------------- 11 files changed, 777 insertions(+), 589 deletions(-) create mode 100644 image-grid.c create mode 100644 tmux-image.h diff --git a/Makefile.am b/Makefile.am index 5a561819e..d79708ab6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -210,6 +210,7 @@ dist_tmux_SOURCES = \ style.c \ tmux-protocol.h \ tmux.c \ + tmux-image.h \ tmux.h \ tty-acs.c \ tty-draw.c \ @@ -251,7 +252,7 @@ endif # Enable image support. if ENABLE_IMAGES -dist_tmux_SOURCES += image.c image-sixel.c +dist_tmux_SOURCES += image.c image-grid.c image-sixel.c endif # Add bits for fuzzing if enabled. diff --git a/grid.c b/grid.c index 830929c0e..d6f67d0b7 100644 --- a/grid.c +++ b/grid.c @@ -308,6 +308,9 @@ grid_free_line(struct grid *gd, u_int py) { struct grid_line *gl = &gd->linedata[py]; +#ifdef ENABLE_IMAGES + image_grid_free_line(gd, gl); +#endif free(gl->celldata); free(gl->extddata); memset(gl, 0, sizeof *gl); @@ -347,6 +350,9 @@ void grid_destroy(struct grid *gd) { grid_free_lines(gd, 0, gd->hsize + gd->sy); +#ifdef ENABLE_IMAGES + image_grid_free(gd); +#endif free(gd->linedata); free(gd); } @@ -692,6 +698,9 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg) return; if (grid_check_y(gd, __func__, py + ny - 1) != 0) return; +#ifdef ENABLE_IMAGES + image_grid_remove(gd, px, py, nx, ny); +#endif for (yy = py; yy < py + ny; yy++) { gl = &gd->linedata[yy]; @@ -794,6 +803,9 @@ grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx, grid_expand_line(gd, py, px + nx, 8); grid_expand_line(gd, py, dx + nx, 8); +#ifdef ENABLE_IMAGES + image_grid_move_cells(gd, dx, px, py, nx); +#endif memmove(&gl->celldata[dx], &gl->celldata[px], nx * sizeof *gl->celldata); if (dx + nx > gl->cellused) @@ -1233,6 +1245,9 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, { struct grid_line *dstl, *srcl; u_int yy; +#ifdef ENABLE_IMAGES + u_int ody = dy, osy = sy; +#endif if (dy + ny > dst->hsize + dst->sy) ny = dst->hsize + dst->sy - dy; @@ -1245,6 +1260,9 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, dstl = &dst->linedata[dy]; memcpy(dstl, srcl, sizeof *dstl); +#ifdef ENABLE_IMAGES + dstl->images = NULL; +#endif if (srcl->cellsize != 0) { dstl->celldata = xreallocarray(NULL, srcl->cellsize, sizeof *dstl->celldata); @@ -1264,6 +1282,9 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, sy++; dy++; } +#ifdef ENABLE_IMAGES + image_grid_duplicate_lines(dst, ody, src, osy, ny); +#endif } /* Mark line as dead. */ @@ -1384,6 +1405,11 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, break; line = yy + 1 + lines; +#ifdef ENABLE_IMAGES + if (image_grid_line_has_images(&gd->linedata[line])) + break; +#endif + /* Consume empty lines, including any markers. */ if (~gd->linedata[line].flags & GRID_LINE_WRAPPED) wrapped = 0; @@ -1453,8 +1479,7 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, /* Remove the lines that were completely consumed. */ for (i = yy + 1; i < yy + 1 + lines; i++) { - free(gd->linedata[i].celldata); - free(gd->linedata[i].extddata); + grid_free_line(gd, i); grid_reflow_dead(&gd->linedata[i]); } @@ -1562,6 +1587,13 @@ grid_reflow(struct grid *gd, u_int sx) if (gl->flags & GRID_LINE_DEAD) continue; +#ifdef ENABLE_IMAGES + if (image_grid_line_has_images(gl)) { + grid_reflow_move(target, gl); + continue; + } +#endif + /* * Work out the width of this line. at is the point at which * the available width is hit, and width is the full line diff --git a/image-grid.c b/image-grid.c new file mode 100644 index 000000000..3c87505d2 --- /dev/null +++ b/image-grid.c @@ -0,0 +1,382 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Michael Grant + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* A run of cells on one grid line. */ +struct image_span { + u_int x; + u_int sx; + u_int source_x; + u_int source_y; + + struct image_line *line; + struct image_placement *placement; + + TAILQ_ENTRY(image_span) line_entry; + TAILQ_ENTRY(image_span) placement_entry; +}; +TAILQ_HEAD(image_spans, image_span); + +/* Spans on one grid line. */ +struct image_line { + struct image_spans spans; +}; + +/* A placement of an image, may be across multiple lines */ +struct image_placement { + struct image_store *store; + struct image *image; + struct image_spans spans; + + TAILQ_ENTRY(image_placement) entry; +}; +TAILQ_HEAD(image_placements, image_placement); + +/* Placements owned by one grid. */ +struct image_store { + struct image_placements placements; +}; + +/* A shared placements copied when duplicating lines. */ +struct image_copy_map { + struct image_placement *source; + struct image_placement *destination; +}; + +/* Part of a span waiting to be moved. */ +struct image_move { + struct image_placement *placement; + u_int x; + u_int sx; + u_int source_x; + u_int source_y; +}; + +/* Get the placement store for a grid. */ +static struct image_store * +image_store_get(struct grid *gd) +{ + if (gd->images == NULL) { + gd->images = xcalloc(1, sizeof *gd->images); + TAILQ_INIT(&gd->images->placements); + } + return (gd->images); +} + +/* Get the spans for a grid line. */ +static struct image_line * +image_line_get(struct grid_line *gl) +{ + if (gl->images == NULL) { + gl->images = xcalloc(1, sizeof *gl->images); + TAILQ_INIT(&gl->images->spans); + } + return (gl->images); +} + +/* Create a placement. Takes a reference to the image. */ +static struct image_placement * +image_placement_create(struct grid *gd, struct image *im) +{ + struct image_store *store = image_store_get(gd); + struct image_placement *plmt; + + plmt = xcalloc(1, sizeof *plmt); + plmt->store = store; + plmt->image = im; + TAILQ_INIT(&plmt->spans); + TAILQ_INSERT_TAIL(&store->placements, plmt, entry); + image_ref(im); + return (plmt); +} + +/* Free a placement after all its spans have gone. */ +static void +image_placement_free(struct image_placement *plmt) +{ + TAILQ_REMOVE(&plmt->store->placements, plmt, entry); + image_free(plmt->image); + free(plmt); +} + +/* Attach a span to both its grid line and its placement. */ +static void +image_span_add(struct image_line *line, struct image_placement *plmt, u_int x, + u_int sx, u_int src_x, u_int src_y) +{ + struct image_span *span; + + if (sx == 0) + return; + + span = xcalloc(1, sizeof *span); + span->x = x; + span->sx = sx; + span->source_x = src_x; + span->source_y = src_y; + span->line = line; + span->placement = plmt; + TAILQ_INSERT_TAIL(&line->spans, span, line_entry); + TAILQ_INSERT_TAIL(&plmt->spans, span, placement_entry); +} + +/* Detach and free one span. */ +static void +image_span_free(struct image_span *span) +{ + TAILQ_REMOVE(&span->line->spans, span, line_entry); + TAILQ_REMOVE(&span->placement->spans, span, placement_entry); + free(span); +} + +/* Free placements with no remaining spans. */ +static void +image_store_prune(struct image_store *store) +{ + struct image_placement *plmt, *next; + + if (store == NULL) + return; + TAILQ_FOREACH_SAFE(plmt, &store->placements, entry, next) { + if (TAILQ_EMPTY(&plmt->spans)) + image_placement_free(plmt); + } +} + +/* Add a source rectangle at absolute grid coordinates. */ +void +image_grid_add(struct grid *gd, struct image *im, u_int x, u_int y, + u_int src_x, u_int src_y, u_int sx, u_int sy) +{ + struct image_placement *plmt; + struct image_line *line; + u_int rows = gd->hsize + gd->sy, row; + u_int image_sx, image_sy; + + if (sx == 0 || sy == 0 || x >= gd->sx || y >= rows) + return; + image_get_size_in_cells(im, &image_sx, &image_sy); + if (src_x >= image_sx || src_y >= image_sy) + return; + + if (sx > gd->sx - x) + sx = gd->sx - x; + if (sx > image_sx - src_x) + sx = image_sx - src_x; + if (sy > rows - y) + sy = rows - y; + if (sy > image_sy - src_y) + sy = image_sy - src_y; + + plmt = image_placement_create(gd, im); + for (row = 0; row < sy; row++) { + line = image_line_get(&gd->linedata[y + row]); + image_span_add(line, plmt, x, sx, src_x, src_y + row); + } +} + +/* Remove the covered part of each span, retaining either side. */ +static void +image_line_remove(struct image_line *line, u_int x, u_int w) +{ + struct image_span *span, *next; + u_int end, span_end, right; + + if (line == NULL || w == 0) + return; + end = x + w; + if (end < x) + end = UINT_MAX; + TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) { + span_end = span->x + span->sx; + if (span_end <= x || span->x >= end) + continue; + 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 a rectangle of image spans from a grid. */ +void +image_grid_remove(struct grid *gd, u_int x, u_int y, u_int sx, u_int sy) +{ + u_int rows = gd->hsize + gd->sy; + u_int row; + + if (gd->images == NULL || sx == 0 || sy == 0 || y >= rows) + return; + if (sy > rows - y) + sy = rows - y; + for (row = y; row < y + sy; row++) + image_line_remove(gd->linedata[row].images, x, sx); + image_store_prune(gd->images); +} + +/* Free all spans on a grid line. */ +void +image_grid_free_line(struct grid *gd, struct grid_line *gl) +{ + struct image_line *line = gl->images; + struct image_span *span, *next; + + if (line == NULL) + return; + + TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) + image_span_free(span); + free(line); + + gl->images = NULL; + image_store_prune(gd->images); +} + +/* Free a grid's empty placement store. */ +void +image_grid_free(struct grid *gd) +{ + if (gd->images != NULL) { + image_store_prune(gd->images); + free(gd->images); + gd->images = NULL; + } +} + +/* Move the image spans belonging to a range of text cells. */ +void +image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx) +{ + struct image_line *line; + struct image_span *span; + struct image_move *moves = NULL; + size_t n = 0, i; + u_int start, end, span_end; + + if (gd->images == NULL || + nx == 0 || + px == dx || + py >= gd->hsize + gd->sy) + return; + line = gd->linedata[py].images; + if (line == NULL) + return; + + end = px + nx; + TAILQ_FOREACH(span, &line->spans, line_entry) { + span_end = span->x + span->sx; + start = (span->x > px ? span->x : px); + if (start >= end || span_end <= px) + continue; + if (span_end > end) + span_end = end; + + moves = xreallocarray(moves, n + 1, sizeof *moves); + moves[n].placement = span->placement; + moves[n].x = dx + start - px; + moves[n].sx = span_end - start; + moves[n].source_x = span->source_x + start - span->x; + moves[n].source_y = span->source_y; + n++; + } + image_line_remove(line, px, nx); + image_line_remove(line, dx, nx); + for (i = 0; i < n; i++) { + image_span_add(line, moves[i].placement, moves[i].x, + moves[i].sx, moves[i].source_x, moves[i].source_y); + } + free(moves); + image_store_prune(gd->images); +} + +/* Map one source placement to one destination placement during a copy. */ +static struct image_placement * +image_copy_placement(struct grid *dst, struct image_placement *src, + struct image_copy_map **maps, u_int *nmaps) +{ + struct image_placement *plmt; + struct image_copy_map *map = *maps; + u_int i, n = *nmaps; + + for (i = 0; i < n; i++) { + if (map[i].source == src) + return (map[i].destination); + } + plmt = image_placement_create(dst, src->image); + + map = xreallocarray(map, n + 1, sizeof *map); + map[n].source = src; + map[n].destination = plmt; + + *maps = map; + *nmaps = n + 1; + return (plmt); +} + +/* Duplicate image spans and their placements between grid lines. */ +void +image_grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, + u_int sy, u_int ny) +{ + struct image_copy_map *maps = NULL; + struct image_line *src_line, *dst_line; + struct image_span *span; + struct image_placement *plmt; + u_int nmaps = 0, row; + + for (row = 0; row < ny; row++) { + src_line = src->linedata[sy + row].images; + if (src_line == NULL) + continue; + dst_line = image_line_get(&dst->linedata[dy + row]); + TAILQ_FOREACH(span, &src_line->spans, line_entry) { + plmt = image_copy_placement(dst, span->placement, &maps, + &nmaps); + image_span_add(dst_line, plmt, span->x, span->sx, + span->source_x, span->source_y); + } + } + free(maps); +} + +/* Check whether a grid line has any image spans. */ +int +image_grid_line_has_images(struct grid_line *gl) +{ + return (gl->images != NULL && !TAILQ_EMPTY(&gl->images->spans)); +} diff --git a/image.c b/image.c index 5613cf920..29875c71a 100644 --- a/image.c +++ b/image.c @@ -2,6 +2,7 @@ /* * Copyright (c) 2007 Nicholas Marriott + * Copyright (c) 2026 Michael Grant * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -18,210 +19,289 @@ #include +#include #include #include #include "tmux.h" -static struct images all_images = TAILQ_HEAD_INITIALIZER(all_images); -static u_int all_images_count; -#define MAX_IMAGE_COUNT 20 +/* Maximum image size in bytes. */ +#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) -static void printflike(3, 4) -image_log(struct image *im, const char* from, const char* fmt, ...) +/* An image. */ +struct image { + u_int id; + u_int references; + u_int flags; +#define IMAGE_FLAG_OWN_PIXELS 0x1 + + struct image *parent; + u_int source_id; + u_int order_id; + + /* Size in cells. */ + u_int sx; + u_int sy; + + /* Size in pixels. */ + u_int width; + u_int height; + + /* Canvas size in pixels. */ + u_int canvas_width; + u_int canvas_height; + + /* + * RGBA pixels use four bytes each. Stride is the number of bytes + * between the start of each row. Size is the number of bytes + * containing all of this image's pixels - it may include pixels that + * are part of the parent image but not part of this image. + */ + size_t stride; + size_t size; + u_char *pixels; + + RB_ENTRY(image) entry; +}; +RB_HEAD(images, image); +static struct images images = RB_INITIALIZER(&images); +static u_int image_next_id; + +/* Compare images by IDs. */ +static int +image_cmp(struct image *a, struct image *b) { - va_list ap; - char s[128]; + if (a->id < b->id) + return (-1); + if (a->id > b->id) + return (1); + return (0); +} +RB_GENERATE_STATIC(images, image, entry, image_cmp); - if (log_get_level() == 0) - return; +/* Find an image by its ID. */ +struct image * +image_find(u_int id) +{ + struct image find; - if (fmt == NULL) { - log_debug("%s: %p (%ux%u %u,%u)", from, im, im->sx, im->sy, - im->px, im->py); - return; - } - - va_start(ap, fmt); - vsnprintf(s, sizeof s, fmt, ap); - va_end(ap); - - log_debug("%s: %p (%ux%u %u,%u): %s", from, im, im->sx, im->sy, - im->px, im->py, s); + find.id = id; + return (RB_FIND(images, &images, &find)); } -static void -image_free(struct image *im) +/* Return the ID of an image. */ +u_int +image_get_id(struct image *im) { - image_log(im, __func__, NULL); - - TAILQ_REMOVE(&all_images, im, all_entry); - all_images_count--; - - TAILQ_REMOVE(im->list, im, entry); - sixel_free(im->data); - free(im->fallback); - free(im); + return (im->id); } -int -image_free_all(struct screen *s) -{ - struct image *im, *im1; - int redraw = !TAILQ_EMPTY(&s->images); - - if (redraw) - log_debug ("%s", __func__); - TAILQ_FOREACH_SAFE(im, &s->images, entry, im1) - image_free(im); - return (redraw); -} - -/* Create text placeholder for an image. */ -static void -image_fallback(char **ret, u_int sx, u_int sy) -{ - char *buf, *label; - u_int py, size, lsize; - - if (sy == 0) { - *ret = xstrdup(""); - return; - } - - /* Allocate first line. */ - lsize = xasprintf(&label, "SIXEL IMAGE (%ux%u)\r\n", sx, sy) + 1; - if (sx < lsize - 3) - size = lsize - 1; - else - size = sx + 2; - - /* Remaining lines. Every placeholder line has \r\n at the end. */ - size += (sx + 2) * (sy - 1) + 1; - *ret = buf = xmalloc(size); - - /* Render first line. */ - if (sx < lsize - 3) { - memcpy(buf, label, lsize); - buf += lsize - 1; - } else { - memcpy(buf, label, lsize - 3); - buf += lsize - 3; - memset(buf, '+', sx - lsize + 3); - buf += sx - lsize + 3; - snprintf(buf, 3, "\r\n"); - buf += 2; - } - - /* Remaining lines. */ - for (py = 1; py < sy; py++) { - memset(buf, '+', sx); - buf += sx; - snprintf(buf, 3, "\r\n"); - buf += 2; - } - - free(label); -} - -struct image* -image_store(struct screen *s, struct sixel_image *si) +/* Allocate an image and assign a unique ID. */ +static struct image * +image_alloc(u_int w, u_int h, u_int cw, u_int ch, u_int sx, u_int sy, + size_t stride, u_char *pixels) { struct image *im; + u_int id; + + do { + if (++image_next_id == 0) + image_next_id++; + id = image_next_id; + } while (image_find(id) != NULL); im = xcalloc(1, sizeof *im); - im->s = s; - im->data = si; - - im->px = s->cx; - im->py = s->cy; - sixel_size_in_cells(si, &im->sx, &im->sy); - - image_fallback(&im->fallback, im->sx, im->sy); - - image_log(im, __func__, NULL); - im->list = &s->images; - TAILQ_INSERT_TAIL(im->list, im, entry); - - TAILQ_INSERT_TAIL(&all_images, im, all_entry); - if (++all_images_count == MAX_IMAGE_COUNT) - image_free(TAILQ_FIRST(&all_images)); - + im->id = id; + im->references = 1; + im->source_id = im->order_id = id; + im->width = w; + im->height = h; + im->canvas_width = cw; + im->canvas_height = ch; + im->sx = sx; + im->sy = sy; + im->stride = stride; + im->size = (h - 1) * stride + w * 4; + im->pixels = pixels; + RB_INSERT(images, &images, im); return (im); } -int -image_check_line(struct screen *s, u_int py, u_int ny) +/* Create an image with the provided pixels. */ +struct image * +image_create(u_int w, u_int h, u_int cw, u_int ch, u_int sx, u_int sy, + u_char *pixels) { - struct image *im, *im1; - int redraw = 0, in; + struct image *im; - TAILQ_FOREACH_SAFE(im, &s->images, entry, im1) { - in = (py + ny > im->py && py < im->py + im->sy); - image_log(im, __func__, "py=%u, ny=%u, in=%d", py, ny, in); - if (in) { - image_free(im); - redraw = 1; - } - } - return (redraw); + if (w == 0 || h == 0 || cw < w || ch < h) + return (NULL); + if (sx == 0 || sy == 0 || pixels == NULL) + return (NULL); + if ((uint64_t)w * h > IMAGE_SIZE_LIMIT / 4) + return (NULL); + if (sx > USHRT_MAX || sy > USHRT_MAX) + return (NULL); + if (sx * sy > IMAGE_SIZE_LIMIT) + return (NULL); + + im = image_alloc(w, h, cw, ch, sx, sy, w * 4, pixels); + im->flags |= IMAGE_FLAG_OWN_PIXELS; + return (im); } -int -image_check_area(struct screen *s, u_int px, u_int py, u_int nx, u_int ny) +/* Resample the crop into transparent, padded RGBA pixels. */ +static u_char * +image_view_resample(struct image *src, struct image_view *view, u_int w, + u_int h) { - struct image *im, *im1; - int redraw = 0, in; + u_char *pixels, *row, *src_row; + u_int src_x, src_y; + uint64_t x, y, off; - TAILQ_FOREACH_SAFE(im, &s->images, entry, im1) { - in = (py < im->py + im->sy && - py + ny > im->py && - px < im->px + im->sx && - px + nx > im->px); - image_log(im, __func__, "py=%u, ny=%u, in=%d", py, ny, in); - if (in) { - image_free(im); - redraw = 1; + pixels = xcalloc(w * h, 4); + for (y = 0; y < view->scaled_height; y++) { + src_y = view->y + y * view->height / view->scaled_height; + src_row = src->pixels + src_y * src->stride + view->x * 4; + + off = ((y + view->canvas_yoff) * w + view->canvas_xoff) * 4; + row = pixels + off; + + if (view->scaled_width == view->width) + memcpy(row, src_row, view->width * 4); + else { + for (x = 0; x < view->scaled_width; x++) { + src_x = x * view->width / view->scaled_width; + memcpy(row + x * 4, src_row + src_x * 4, 4); + } } } - return (redraw); + return (pixels); } -int -image_scroll_up(struct screen *s, u_int lines) +/* Crop and resample an image. The view takes a reference to the image. */ +struct image * +image_create_view(struct image *src, struct image_view *view) { - struct image *im, *im1; - int redraw = 0; - u_int sx, sy; - struct sixel_image *new; + struct image *im; + u_char *pixels; + u_int w, h; - TAILQ_FOREACH_SAFE(im, &s->images, entry, im1) { - if (im->py >= lines) { - image_log(im, __func__, "1, lines=%u", lines); - im->py -= lines; - redraw = 1; - continue; - } - if (im->py + im->sy <= lines) { - image_log(im, __func__, "2, lines=%u", lines); - image_free(im); - redraw = 1; - continue; - } - sx = im->sx; - sy = (im->py + im->sy) - lines; - image_log(im, __func__, "3, lines=%u, sy=%u", lines, sy); + if (src == NULL || + view == NULL || + view->x >= src->width || + view->y >= src->height || + view->width == 0 || + view->height == 0 || + view->scaled_width == 0 || + view->scaled_height == 0 || + view->sx == 0 || + view->sy == 0 || + view->width > src->width - view->x || + view->height > src->height - view->y || + view->sx > USHRT_MAX || + view->sy > USHRT_MAX || + view->sx * view->sy > IMAGE_SIZE_LIMIT || + view->canvas_xoff > view->canvas_width || + view->scaled_width > view->canvas_width - view->canvas_xoff || + view->canvas_yoff > view->canvas_height || + view->scaled_height > view->canvas_height - view->canvas_yoff) + return (NULL); - new = sixel_scale(im->data, 0, 0, 0, im->sy - sy, sx, sy, 1); - sixel_free(im->data); - im->data = new; - - im->py = 0; - sixel_size_in_cells(im->data, &im->sx, &im->sy); - - free(im->fallback); - image_fallback(&im->fallback, im->sx, im->sy); - redraw = 1; + if (view->scaled_width == view->width && + view->scaled_height == view->height && + view->canvas_xoff == 0 && + view->canvas_yoff == 0) { + pixels = src->pixels + view->y * src->stride + view->x * 4; + im = image_alloc(view->width, view->height, view->canvas_width, + view->canvas_height, view->sx, view->sy, src->stride, + pixels); + } else { + w = view->canvas_xoff + view->scaled_width; + h = view->canvas_yoff + view->scaled_height; + if ((uint64_t)w * h > IMAGE_SIZE_LIMIT / 4) + return (NULL); + pixels = image_view_resample(src, view, w, h); + im = image_alloc(w, h, view->canvas_width, view->canvas_height, + view->sx, view->sy, w * 4, pixels); + im->flags |= IMAGE_FLAG_OWN_PIXELS; } - return (redraw); + im->parent = src; + im->source_id = src->source_id; + im->order_id = src->order_id; + image_ref(src); + return (im); +} + +/* Add a reference to an image. */ +void +image_ref(struct image *im) +{ + if (im->references == UINT_MAX) + fatalx("invalid image reference %u", im->id); + im->references++; +} + +/* Free an image. */ +void +image_free(struct image *im) +{ + if (im->references == 0) + fatalx("invalid image reference %u", im->id); + if (--im->references == 0) { + RB_REMOVE(images, &images, im); + if (im->flags & IMAGE_FLAG_OWN_PIXELS) + free(im->pixels); + if (im->parent != NULL) + image_free(im->parent); + free(im); + } +} + +/* Return image size in pixels. */ +void +image_get_size(struct image *im, u_int *w, u_int *h) +{ + *w = im->width; + *h = im->height; +} + +/* Return canvas size in pixels. */ +void +image_get_canvas_size(struct image *im, u_int *w, u_int *h) +{ + *w = im->canvas_width; + *h = im->canvas_height; +} + +/* Return image size in cells. */ +void +image_get_size_in_cells(struct image *im, u_int *sx, u_int *sy) +{ + *sx = im->sx; + *sy = im->sy; +} + +/* Return image pixels. */ +const u_char * +image_get_pixels(struct image *im, size_t *stride, size_t *size) +{ + if (stride != NULL) + *stride = im->stride; + if (size != NULL) + *size = im->size; + return (im->pixels); +} + +/* Calculate the cell size. */ +void +image_size_in_cells(u_int w, u_int h, u_int xpixel, u_int ypixel, u_int *sx, + u_int *sy) +{ + if (xpixel == 0) + xpixel = 8; + if (ypixel == 0) + ypixel = 16; + *sx = w / xpixel + (w % xpixel != 0); + *sy = h / ypixel + (h % ypixel != 0); } diff --git a/input.c b/input.c index 9bd03cf4e..f2f1db535 100644 --- a/input.c +++ b/input.c @@ -2625,11 +2625,6 @@ input_dcs_dispatch(struct input_ctx *ictx) const char prefix[] = "tmux;"; const u_int prefixlen = (sizeof prefix) - 1; long long allow_passthrough = 0; -#ifdef ENABLE_IMAGES - struct window *w; - struct sixel_image *si; - int p2; -#endif if (wp == NULL) oo = global_w_options; @@ -2641,20 +2636,6 @@ input_dcs_dispatch(struct input_ctx *ictx) return (0); } -#ifdef ENABLE_IMAGES - if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) { - w = wp->window; - if (input_split(ictx) != 0) - return (0); - p2 = input_get(ictx, 1, 0, 0); - if (p2 == -1) - p2 = 0; - si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel); - if (si != NULL) - screen_write_sixelimage(sctx, si, ictx->cell.cell.bg); - } -#endif - /* DCS sequences with intermediate byte '$' (includes DECRQSS). */ if (ictx->interm_len == 1 && ictx->interm_buf[0] == '$') { /* DECRQSS is DCS $ q Pt ST. */ diff --git a/screen-redraw.c b/screen-redraw.c index 9d9704f98..b50b6df65 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1921,15 +1921,6 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) tty_reset(tty); -#ifdef ENABLE_IMAGES - if (wp != NULL) - tty_draw_images(c, wp); - else { - TAILQ_FOREACH(loop, &scene->w->panes, entry) - tty_draw_images(c, loop); - } -#endif - log_debug("%s: finished @%u redraw", c->name, scene->w->id); } diff --git a/screen-write.c b/screen-write.c index bc2cedd2c..b37dd6283 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1471,11 +1471,6 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) memcpy(&gc, &grid_default_cell, sizeof gc); utf8_set(&gc.data, 'E'); -#ifdef ENABLE_IMAGES - if (image_free_all(s) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - for (yy = 0; yy < screen_size_y(s); yy++) { for (xx = 0; xx < screen_size_x(s); xx++) grid_view_set_cell(s->grid, xx, yy, &gc); @@ -1518,11 +1513,6 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - screen_write_initctx(ctx, &ttyctx, 0, 1); ttyctx.bg = bg; @@ -1559,11 +1549,6 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - screen_write_initctx(ctx, &ttyctx, 0, 1); ttyctx.bg = bg; @@ -1600,11 +1585,6 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - screen_write_initctx(ctx, &ttyctx, 0, 1); ttyctx.bg = bg; @@ -1635,11 +1615,6 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg) if (ny == 0) ny = 1; -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - if (s->cy < s->rupper || s->cy > s->rlower) { if (ny > sy - s->cy) ny = sy - s->cy; @@ -1703,11 +1678,6 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) if (ny == 0) ny = 1; -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - if (s->cy < s->rupper || s->cy > s->rlower) { if (ny > sy - s->cy) ny = sy - s->cy; @@ -1776,11 +1746,6 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg) if (gl->cellsize == 0 && COLOUR_DEFAULT(bg)) return; -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - flags = gl->flags & GRID_LINE_OSC133_FLAGS; memcpy(&od, &gl->osc133_data, sizeof od); grid_view_clear(s->grid, 0, s->cy, sx, 1, bg); @@ -1815,11 +1780,6 @@ screen_write_clearendofline(struct screen_write_ctx *ctx, u_int bg) if (s->cx > sx - 1 || (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg))) return; -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg); ci->x = s->cx; @@ -1842,11 +1802,6 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg) return; } -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - if (s->cx > sx - 1) grid_view_clear(s->grid, 0, s->cy, sx, 1, bg); else @@ -1896,11 +1851,6 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg) return; } -#ifdef ENABLE_IMAGES - if (image_free_all(s) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1948,9 +1898,6 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg) struct screen *s = ctx->s; struct grid *gd = s->grid; struct grid_line *gl; -#ifdef ENABLE_IMAGES - int redraw = 0; -#endif u_int rupper = s->rupper, rlower = s->rlower; gl = grid_get_line(gd, gd->hsize + s->cy); @@ -1971,15 +1918,6 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg) return; } -#ifdef ENABLE_IMAGES - if (rlower == screen_size_y(s) - 1) - redraw = image_scroll_up(s, 1); - else - redraw = image_check_line(s, rupper, rlower - rupper); - if (redraw && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg); screen_write_collect_scroll(ctx, bg); ctx->scrolled++; @@ -2003,11 +1941,6 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg) ctx->bg = bg; } -#ifdef ENABLE_IMAGES - if (image_scroll_up(s, lines) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - for (i = 0; i < lines; i++) { grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg); screen_write_collect_scroll(ctx, bg); @@ -2032,11 +1965,6 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg) else if (lines > s->rlower - s->rupper + 1) lines = s->rlower - s->rupper + 1; -#ifdef ENABLE_IMAGES - if (image_free_all(s) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - for (i = 0; i < lines; i++) grid_view_scroll_region_down(gd, s->rupper, s->rlower, bg); @@ -2073,11 +2001,6 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; -#ifdef ENABLE_IMAGES - if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; @@ -2154,11 +2077,6 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; -#ifdef ENABLE_IMAGES - if (image_check_line(s, 0, s->cy - 1) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; @@ -2227,11 +2145,6 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; -#ifdef ENABLE_IMAGES - if (image_free_all(s) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; @@ -2735,11 +2648,6 @@ screen_write_collect_end(struct screen_write_ctx *ctx) } } -#ifdef ENABLE_IMAGES - if (image_check_area(s, s->cx, s->cy, ci->used, 1) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -#endif - grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, ci->used); if (bnx != 0) @@ -3239,66 +3147,6 @@ screen_write_rawstring(struct screen_write_ctx *ctx, u_char *str, u_int len, tty_write(tty_cmd_rawstring, &ttyctx); } -#ifdef ENABLE_IMAGES -/* Write a SIXEL image. */ -void -screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si, - u_int bg) -{ - struct screen *s = ctx->s; - struct grid *gd = s->grid; - struct tty_ctx ttyctx; - u_int x, y, sx, sy, cx = s->cx, cy = s->cy, i, lines; - struct sixel_image *new; - - if (screen_size_y(s) == 1) - return; - - sixel_size_in_cells(si, &x, &y); - if (x > screen_size_x(s) || y > screen_size_y(s) - 1) { - if (x > screen_size_x(s) - cx) - sx = screen_size_x(s) - cx; - else - sx = x; - if (y > screen_size_y(s) - 1) - sy = screen_size_y(s) - 1; - else - sy = y; - new = sixel_scale(si, 0, 0, 0, y - sy, sx, sy, 1); - sixel_free(si); - if (new == NULL) - return; - sixel_size_in_cells(new, &x, &y); - si = new; - } - - sy = screen_size_y(s) - cy; - if (sy <= y) { - lines = y - sy + 1; - if (image_scroll_up(s, lines) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; - for (i = 0; i < lines; i++) { - grid_view_scroll_region_up(gd, 0, screen_size_y(s) - 1, - bg); - screen_write_collect_scroll(ctx, bg); - } - ctx->scrolled += lines; - if (lines > cy) - screen_write_cursormove(ctx, -1, 0, 0); - else - screen_write_cursormove(ctx, -1, cy - lines, 0); - } - screen_write_collect_flush(ctx, 0, __func__); - - screen_write_initctx(ctx, &ttyctx, 0, 0); - ttyctx.image = image_store(s, si); - - tty_write(tty_cmd_sixelimage, &ttyctx); - - screen_write_cursormove(ctx, 0, cy + y, 0); -} -#endif - /* Turn alternate screen on. */ void screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, diff --git a/screen.c b/screen.c index e8d90d595..2a433aaf0 100644 --- a/screen.c +++ b/screen.c @@ -93,11 +93,6 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->tabs = NULL; s->sel = NULL; -#ifdef ENABLE_IMAGES - TAILQ_INIT(&s->images); - TAILQ_INIT(&s->saved_images); -#endif - s->write_list = NULL; s->hyperlinks = NULL; @@ -129,11 +124,6 @@ screen_reinit(struct screen *s) screen_clear_selection(s); screen_free_titles(s); - -#ifdef ENABLE_IMAGES - image_free_all(s); -#endif - screen_set_progress_bar(s, PROGRESS_BAR_HIDDEN, 0); screen_reset_hyperlinks(s); } @@ -152,10 +142,6 @@ screen_reset_hyperlinks(struct screen *s) void screen_free(struct screen *s) { -#ifdef ENABLE_IMAGES - struct image *im; -#endif - free(s->sel); free(s->tabs); free(s->path); @@ -172,17 +158,6 @@ screen_free(struct screen *s) hyperlinks_free(s->hyperlinks); screen_free_titles(s); -#ifdef ENABLE_IMAGES - /* - * Images saved when entering the alternate screen stay linked in the - * global list; move them back so they are freed and unlinked here, or - * a later eviction would write through this freed screen. - */ - TAILQ_CONCAT(&s->images, &s->saved_images, entry); - TAILQ_FOREACH(im, &s->images, entry) - im->list = &s->images; - image_free_all(s); -#endif } /* Reset tabs to default, eight spaces apart. */ @@ -375,10 +350,6 @@ screen_resize_cursor(struct screen *s, u_int sx, u_int sy, int reflow, if (sy != screen_size_y(s)) screen_resize_y(s, sy, eat_empty, &cy); -#ifdef ENABLE_IMAGES - image_free_all(s); -#endif - if (reflow) screen_reflow(s, sx, &cx, &cy, cursor); @@ -690,10 +661,6 @@ int screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor) { u_int sx, sy; -#ifdef ENABLE_IMAGES - struct image *im; -#endif - if (SCREEN_IS_ALTERNATE(s)) return 0; sx = screen_size_x(s); @@ -707,12 +674,6 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor) } memcpy(&s->saved_cell, gc, sizeof s->saved_cell); -#ifdef ENABLE_IMAGES - TAILQ_CONCAT(&s->saved_images, &s->images, entry); - TAILQ_FOREACH(im, &s->saved_images, entry) - im->list = &s->saved_images; -#endif - grid_view_clear(s->grid, 0, 0, sx, sy, 8); s->saved_flags = s->grid->flags; @@ -726,10 +687,6 @@ int screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) { u_int sx = screen_size_x(s), sy = screen_size_y(s); -#ifdef ENABLE_IMAGES - struct image *im; -#endif - /* * If the current size is different, temporarily resize to the old size * before copying back. @@ -776,13 +733,6 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) grid_destroy(s->saved_grid); s->saved_grid = NULL; -#ifdef ENABLE_IMAGES - image_free_all(s); - TAILQ_CONCAT(&s->images, &s->saved_images, entry); - TAILQ_FOREACH(im, &s->images, entry) - im->list = &s->images; -#endif - if (s->cx > screen_size_x(s) - 1) s->cx = screen_size_x(s) - 1; if (s->cy > screen_size_y(s) - 1) diff --git a/tmux-image.h b/tmux-image.h new file mode 100644 index 000000000..61fb5598b --- /dev/null +++ b/tmux-image.h @@ -0,0 +1,99 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2007 Nicholas Marriott + * Copyright (c) 2026 Michael Grant + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef TMUX_IMAGE_H +#define TMUX_IMAGE_H + +#ifdef ENABLE_IMAGES +#error image support is not ready yet + +struct grid; +struct grid_line; +struct image; +struct image_line; +struct image_store; +struct screen; +struct screen_write_ctx; +struct sixel_image; + +/* View of all or part of an image. */ +struct image_view { + /* Position and size in the image, in pixels. */ + u_int x; + u_int y; + u_int width; + u_int height; + + /* Size after scaling, in pixels. */ + u_int scaled_width; + u_int scaled_height; + + /* Destination canvas size and offset in pixels. */ + u_int canvas_width; + u_int canvas_height; + u_int canvas_xoff; + u_int canvas_yoff; + + /* Destination size in terminal cells. */ + u_int sx; + u_int sy; +}; + +/* image.c */ +struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int, + u_char *); +struct image *image_create_view(struct image *, struct image_view *); +struct image *image_find(u_int); +u_int image_get_id(struct image *); +void image_ref(struct image *); +void image_free(struct image *); +void image_get_size(struct image *, u_int *, u_int *); +void image_get_canvas_size(struct image *, u_int *, u_int *); +void image_get_size_in_cells(struct image *, u_int *, u_int *); +const u_char *image_get_pixels(struct image *, size_t *, size_t *); +void image_size_in_cells(u_int, u_int, u_int, u_int, u_int *, + u_int *); + +/* image-grid.c */ +void image_grid_add(struct grid *, struct image *, u_int, u_int, + u_int, u_int, u_int, u_int); +void image_grid_remove(struct grid *, u_int, u_int, u_int, u_int); +void image_grid_free_line(struct grid *, struct grid_line *); +void image_grid_free(struct grid *); +void image_grid_move_cells(struct grid *, u_int, u_int, u_int, + u_int); +void image_grid_duplicate_lines(struct grid *, u_int, struct grid *, + u_int, u_int); +int image_grid_line_has_images(struct grid_line *); + +/* image-sixel.c */ +#define SIXEL_COLOUR_REGISTERS 1024 +struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int); +void sixel_free(struct sixel_image *); +void sixel_log(struct sixel_image *); +void sixel_size_in_cells(struct sixel_image *, u_int *, u_int *); +struct sixel_image *sixel_scale(struct sixel_image *, u_int, u_int, u_int, + u_int, u_int, u_int, int); +char *sixel_print(struct sixel_image *, struct sixel_image *, + size_t *); +struct screen *sixel_to_screen(struct sixel_image *); + +#endif /* ENABLE_IMAGES */ + +#endif /* TMUX_IMAGE_H */ diff --git a/tmux.h b/tmux.h index 37a504ac8..743e89d38 100644 --- a/tmux.h +++ b/tmux.h @@ -19,11 +19,13 @@ #ifndef TMUX_H #define TMUX_H +#include #include #include #include #include +#include #include #include #include @@ -33,6 +35,7 @@ #endif #include "compat.h" +#include "tmux-image.h" #include "tmux-protocol.h" #include "xmalloc.h" @@ -77,10 +80,6 @@ struct screen_write_cline; struct screen_write_ctx; struct session; -#ifdef ENABLE_IMAGES -struct sixel_image; -#endif - struct tty_ctx; struct tty_code; struct tty_key; @@ -927,6 +926,9 @@ struct grid_line { u_int time; struct osc133_data osc133_data; u_short flags; +#ifdef ENABLE_IMAGES + struct image_line *images; +#endif }; /* Entire grid of cells. */ @@ -946,6 +948,9 @@ struct grid { u_int scroll_generation; struct grid_line *linedata; +#ifdef ENABLE_IMAGES + struct image_store *images; +#endif }; /* Virtual cursor in a grid. */ @@ -1037,26 +1042,6 @@ struct style { u_int link; }; -#ifdef ENABLE_IMAGES -/* Image. */ -struct image { - struct screen *s; - struct sixel_image *data; - char *fallback; - - u_int px; - u_int py; - u_int sx; - u_int sy; - - struct images *list; - TAILQ_ENTRY (image) entry; - - TAILQ_ENTRY (image) all_entry; -}; -TAILQ_HEAD(images, image); -#endif - /* Cursor style. */ enum screen_cursor_style { SCREEN_CURSOR_DEFAULT, @@ -1113,11 +1098,6 @@ struct screen { bitstr_t *tabs; struct screen_sel *sel; -#ifdef ENABLE_IMAGES - struct images images; - struct images saved_images; -#endif - struct screen_write_cline *write_list; struct hyperlinks *hyperlinks; @@ -1905,9 +1885,6 @@ struct tty_ctx { size_t size; } sel; -#ifdef ENABLE_IMAGES - struct image *image; -#endif }; /* @@ -3006,10 +2983,6 @@ void tty_cmd_scrolldown(struct tty *, const struct tty_ctx *); void tty_cmd_reverseindex(struct tty *, const struct tty_ctx *); void tty_cmd_setselection(struct tty *, const struct tty_ctx *); void tty_cmd_rawstring(struct tty *, const struct tty_ctx *); -#ifdef ENABLE_IMAGES -void tty_cmd_sixelimage(struct tty *, const struct tty_ctx *); -void tty_draw_images(struct client *, struct window_pane *); -#endif void tty_cmd_syncstart(struct tty *, const struct tty_ctx *); void tty_default_colours(struct grid_cell *, struct window_pane *, u_int *); @@ -3650,10 +3623,6 @@ void screen_write_setselection(struct screen_write_ctx *, const char *, u_char *, u_int); void screen_write_rawstring(struct screen_write_ctx *, u_char *, u_int, int); -#ifdef ENABLE_IMAGES -void screen_write_sixelimage(struct screen_write_ctx *, - struct sixel_image *, u_int); -#endif void screen_write_alternateon(struct screen_write_ctx *, struct grid_cell *, int); void screen_write_alternateoff(struct screen_write_ctx *, @@ -4238,27 +4207,6 @@ void spawn_editor_finish(struct window_pane *); /* regsub.c */ char *regsub(const char *, const char *, const char *, int); -#ifdef ENABLE_IMAGES -/* image.c */ -int image_free_all(struct screen *); -struct image *image_store(struct screen *, struct sixel_image *); -int image_check_line(struct screen *, u_int, u_int); -int image_check_area(struct screen *, u_int, u_int, u_int, u_int); -int image_scroll_up(struct screen *, u_int); - -/* image-sixel.c */ -#define SIXEL_COLOUR_REGISTERS 1024 -struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int); -void sixel_free(struct sixel_image *); -void sixel_log(struct sixel_image *); -void sixel_size_in_cells(struct sixel_image *, u_int *, u_int *); -struct sixel_image *sixel_scale(struct sixel_image *, u_int, u_int, u_int, - u_int, u_int, u_int, int); -char *sixel_print(struct sixel_image *, struct sixel_image *, - size_t *); -struct screen *sixel_to_screen(struct sixel_image *); -#endif - /* server-acl.c */ #define SERVER_ACL_READONLY 0x1 #define SERVER_ACL_IS_GROUP 0x2 diff --git a/tty.c b/tty.c index 494e5634f..ad2853d42 100644 --- a/tty.c +++ b/tty.c @@ -68,11 +68,6 @@ static void tty_emulate_repeat(struct tty *, enum tty_code_code, enum tty_code_code, u_int); static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int); -#ifdef ENABLE_IMAGES -static void tty_write_one(void (*)(struct tty *, const struct tty_ctx *), - struct client *, struct tty_ctx *); -#endif - #define tty_use_margin(tty) \ (tty->term->flags & TERM_DECSLRM) #define tty_full_width(tty, ctx) \ @@ -1460,60 +1455,6 @@ tty_check_codeset(struct tty *tty, const struct grid_cell *gc) return (&new); } -#ifdef ENABLE_IMAGES -/* Update context for client. */ -static int -tty_set_client_cb(struct tty_ctx *ttyctx, struct client *c) -{ - struct window_pane *wp = ttyctx->arg; - - if (c->session->curw->window != wp->window) - return (0); - if (wp->layout_cell == NULL) - return (0); - - if (tty_window_offset(&c->tty, &ttyctx->wox, &ttyctx->woy, &ttyctx->wsx, - &ttyctx->wsy)) - ttyctx->flags |= TTY_CTX_WINDOW_BIGGER; - else - ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER; - - ttyctx->yoff = ttyctx->ryoff = wp->yoff; - if (status_at_line(c) == 0) - ttyctx->yoff += status_line_size(c); - - return (1); -} - -void -tty_draw_images(struct client *c, struct window_pane *wp) -{ - struct image *im; - struct tty_ctx ttyctx; - - TAILQ_FOREACH(im, &wp->screen->images, entry) { - memset(&ttyctx, 0, sizeof ttyctx); - - /* Set the client independent properties. */ - ttyctx.ocx = im->px; - ttyctx.ocy = im->py; - - ttyctx.orlower = wp->screen->rlower; - ttyctx.orupper = wp->screen->rupper; - - ttyctx.xoff = ttyctx.rxoff = wp->xoff; - ttyctx.sx = wp->sx; - ttyctx.sy = wp->sy; - - ttyctx.image = im; - ttyctx.arg = wp; - ttyctx.set_client_cb = tty_set_client_cb; - ttyctx.flags |= TTY_CTX_INVISIBLE_PANES; - tty_write_one(tty_cmd_sixelimage, c, &ttyctx); - } -} -#endif - void tty_sync_start(struct tty *tty) { @@ -1588,19 +1529,6 @@ tty_write(void (*cmdfn)(struct tty *, const struct tty_ctx *), } } -#ifdef ENABLE_IMAGES -/* Only write to the incoming tty instead of every client. */ -static void -tty_write_one(void (*cmdfn)(struct tty *, const struct tty_ctx *), - struct client *c, struct tty_ctx *ctx) -{ - if (ctx->set_client_cb == NULL) - return; - if ((ctx->set_client_cb(ctx, c)) == 1) - cmdfn(&c->tty, ctx); -} -#endif - void tty_cmd_insertcharacter(struct tty *tty, const struct tty_ctx *ctx) { @@ -2034,58 +1962,6 @@ tty_cmd_rawstring(struct tty *tty, const struct tty_ctx *ctx) tty_invalidate(tty); } -#ifdef ENABLE_IMAGES -void -tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx) -{ - struct image *im = ctx->image; - struct sixel_image *si = im->data; - struct sixel_image *new; - char *data; - size_t size; - u_int cx = ctx->ocx, cy = ctx->ocy, sx, sy; - u_int i, j, x, y, rx, ry; - int fallback = 0; - - if ((~tty->term->flags & TERM_SIXEL) && - !tty_term_has(tty->term, TTYC_SXL)) - fallback = 1; - if (tty->xpixel == 0 || tty->ypixel == 0) - fallback = 1; - - sixel_size_in_cells(si, &sx, &sy); - log_debug("%s: image is %ux%u", __func__, sx, sy); - if (!tty_clamp_area(tty, ctx, cx, cy, sx, sy, &i, &j, &x, &y, &rx, &ry)) - return; - log_debug("%s: clamping to %u,%u-%u,%u", __func__, i, j, rx, ry); - - if (fallback == 1) { - data = xstrdup(im->fallback); - size = strlen(data); - } else { - new = sixel_scale(si, tty->xpixel, tty->ypixel, i, j, rx, ry, 0); - if (new == NULL) - return; - - data = sixel_print(new, si, &size); - } - if (data != NULL) { - log_debug("%s: %zu bytes: %s", __func__, size, data); - tty_region_off(tty); - tty_margin_off(tty); - tty_cursor(tty, x, y); - - tty->flags |= TTY_NOBLOCK; - tty_add(tty, data, size); - tty_invalidate(tty); - free(data); - } - - if (fallback == 0) - sixel_free(new); -} -#endif - void tty_cmd_syncstart(struct tty *tty, const struct tty_ctx *ctx) { From 9551cdcd67325e0ab9ccf0ea06f605eaefca9838 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 19:30:42 +0100 Subject: [PATCH 02/12] Add test for killing own session crash. --- regress/choose-tree.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/regress/choose-tree.sh b/regress/choose-tree.sh index 4656963e6..92325ed33 100644 --- a/regress/choose-tree.sh +++ b/regress/choose-tree.sh @@ -378,4 +378,23 @@ done [ "$i" -lt 50 ] || fail "tagged windows were not killed" exit_mode q +# --- killing the mode's own session ----------------------------------------- +# Killing the attached session also destroys the pane hosting the tree. The +# confirmation callback must finish without redrawing the destroyed mode. +$TMUX set-option -g detach-on-destroy off || exit 1 +$TMUX choose-tree -s -t aaa:0 -F 'G7' -O name || exit 1 +wait_count ': G7' 2 +$TMUX send-keys -t aaa:0 g x || fail "send-keys session kill failed" +wait_for 'Kill session aaa' +$TMUX send-keys -t aaa:0 y || fail "session kill confirmation failed" +i=0 +while [ "$i" -lt 50 ]; do + sessions=$($TMUX list-sessions -F '#{session_name}' 2>/dev/null) + client_session=$($TMUX list-clients -F '#{client_session}' 2>/dev/null) + [ "$sessions" = 'zzz' ] && [ "$client_session" = 'zzz' ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "killing the tree session killed the server" + exit 0 From d913cf5723edec57fd8328bda91302414ffa94b6 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 15:40:17 +0000 Subject: [PATCH 03/12] Use grid_free_line when freeing reflow lines. --- grid.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/grid.c b/grid.c index 8af37d83d..2f9717190 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.160 2026/10/12 08:08:37 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.161 2026/10/12 15:40:17 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1452,8 +1452,7 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, /* Remove the lines that were completely consumed. */ for (i = yy + 1; i < yy + 1 + lines; i++) { - free(gd->linedata[i].celldata); - free(gd->linedata[i].extddata); + grid_free_line(gd, i); grid_reflow_dead(&gd->linedata[i]); } From 7d01314422fe044c41dd3cd21c90e1364533d44a Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 17:04:48 +0000 Subject: [PATCH 04/12] Shorten some long lines. --- input.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/input.c b/input.c index 24d55174b..0d850e732 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.273 2026/10/08 07:50:05 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.274 2026/10/12 17:04:48 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2016,6 +2016,7 @@ static void input_csi_dispatch_sm_private(struct input_ctx *ictx) { struct screen_write_ctx *sctx = &ictx->ctx; + struct window_pane *wp = ictx->wp; struct grid_cell *gc = &ictx->cell.cell; u_int i; @@ -2077,13 +2078,13 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx) break; case 2031: screen_write_mode_set(sctx, MODE_THEME_UPDATES); - if (ictx->wp != NULL) { - ictx->wp->last_theme = window_pane_get_theme(ictx->wp); - ictx->wp->flags &= ~PANE_THEMECHANGED; + if (wp != NULL) { + wp->last_theme = window_pane_get_theme(wp); + wp->flags &= ~PANE_THEMECHANGED; } break; case 2026: - screen_write_start_sync(ictx->wp); + screen_write_start_sync(wp); break; default: log_debug("%s: unknown '%c'", __func__, ictx->ch); From 0985de0bc6fbfb7778b95803beb3f0c22734db84 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 18:23:25 +0000 Subject: [PATCH 05/12] For tree mode, hold a reference to prompt and other bits across command to avoid crashing when killing the session with the mode's pane. GitHub issue 5728. --- mode-tree.c | 35 ++++++++++++++++++++--------------- window-tree.c | 12 +++++------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/mode-tree.c b/mode-tree.c index 222047f61..011294aec 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: mode-tree.c,v 1.103 2026/09/24 18:59:00 nicm Exp $ */ +/* $OpenBSD: mode-tree.c,v 1.104 2026/10/12 18:23:25 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -701,8 +701,17 @@ mode_tree_build(struct mode_tree_data *mtd) static void mode_tree_remove_ref(struct mode_tree_data *mtd) { - if (--mtd->references == 0) - free(mtd); + if (--mtd->references != 0) + return; + + mode_tree_clear_prompt(mtd); + mode_tree_free_items(&mtd->children); + mode_tree_clear_lines(mtd); + screen_free(&mtd->screen); + + free(mtd->search); + free(mtd->filter); + free(mtd); } void @@ -713,14 +722,6 @@ mode_tree_free(struct mode_tree_data *mtd) if (mtd->zoomed == 0) server_unzoom_window(wp->window); - mode_tree_clear_prompt(mtd); - mode_tree_free_items(&mtd->children); - mode_tree_clear_lines(mtd); - screen_free(&mtd->screen); - - free(mtd->search); - free(mtd->filter); - mtd->dead = 1; mode_tree_remove_ref(mtd); } @@ -1095,7 +1096,7 @@ mode_tree_prompt_accept(struct cmdq_item *item, void *data) struct client *c = cmdq_get_client(item); key_code key = 'y'; - if (mtd->prompt != NULL && c != NULL) + if (!mtd->dead && mtd->prompt != NULL && c != NULL) mode_tree_key(mtd, c, &key, NULL, NULL, NULL); mode_tree_remove_ref(mtd); @@ -1122,7 +1123,6 @@ mode_tree_prompt_free_callback(void *data) mtp->mtd->prompt_data = NULL; if (mtp->freecb != NULL) mtp->freecb(mtp->data); - mode_tree_remove_ref(mtp->mtd); free(mtp); } @@ -1152,8 +1152,6 @@ mode_tree_set_prompt(struct mode_tree_data *mtd, struct client *c, mtp->inputcb = inputcb; mtp->freecb = freecb; mtp->data = data; - - mtd->references++; mtd->prompt_top = options_get_number(oo, "status-position") == 0; memset(&pd, 0, sizeof pd); @@ -1524,6 +1522,7 @@ mode_tree_key(struct mode_tree_data *mtd, struct client *c, key_code *key, if (mtd->prompt != NULL) { redraw = 0; prompt = mtd->prompt; + mtd->references++; mtp = mtd->prompt_data; if (mtp != NULL) @@ -1548,6 +1547,11 @@ mode_tree_key(struct mode_tree_data *mtd, struct client *c, key_code *key, } } else result = prompt_key(prompt, *key, &redraw); + if (mtd->dead) { + mode_tree_remove_ref(mtd); + *key = KEYC_NONE; + return (1); + } if (mtd->prompt_data == mtp && mtp != NULL) mtp->c = NULL; @@ -1563,6 +1567,7 @@ mode_tree_key(struct mode_tree_data *mtd, struct client *c, key_code *key, mode_tree_draw(mtd); mtd->wp->flags |= PANE_REDRAW; } + mode_tree_remove_ref(mtd); if (result != PROMPT_KEY_NOT_HANDLED) { *key = KEYC_NONE; return (0); diff --git a/window-tree.c b/window-tree.c index 49f9bbecc..b6388d92d 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-tree.c,v 1.96 2026/08/25 07:23:30 nicm Exp $ */ +/* $OpenBSD: window-tree.c,v 1.97 2026/10/12 18:23:25 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -1344,12 +1344,10 @@ window_tree_kill_current_callback(struct client *c, void *modedata, if (tolower((u_char) s[0]) != 'y' || s[1] != '\0') return (PROMPT_CLOSE); + data->references++; window_tree_kill_each(data, mode_tree_get_current(mtd), c, KEYC_NONE); server_renumber_all(); - - data->references++; cmdq_append(c, cmdq_get_callback(window_tree_command_done, data)); - return (PROMPT_CLOSE); } @@ -1365,12 +1363,10 @@ window_tree_kill_tagged_callback(struct client *c, void *modedata, if (tolower((u_char) s[0]) != 'y' || s[1] != '\0') return (PROMPT_CLOSE); + data->references++; mode_tree_each_tagged(mtd, window_tree_kill_each, c, KEYC_NONE, 1); server_renumber_all(); - - data->references++; cmdq_append(c, cmdq_get_callback(window_tree_command_done, data)); - return (PROMPT_CLOSE); } @@ -1453,6 +1449,8 @@ window_tree_key(struct window_mode_entry *wme, struct client *c, item = mode_tree_get_current(data->data); finished = mode_tree_key(data->data, c, &key, m, &x, &y); + if (data->dead) + return; again: if (item != (new_item = mode_tree_get_current(data->data))) { From e2eb6b7d87aa7eb6e049b5d9658f489da02234ed Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 18:47:52 +0000 Subject: [PATCH 06/12] Add a flag to mark screen_write_ctx as coming from pane input and change a function argument. --- input.c | 5 ++++- screen-write.c | 17 +++++++++-------- tmux.h | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/input.c b/input.c index 0d850e732..8ab63f5f6 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.274 2026/10/12 17:04:48 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.275 2026/10/12 18:47:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -936,6 +936,7 @@ input_reset(struct input_ctx *ictx, int clear) screen_write_start_pane(sctx, wp, &wp->base); else screen_write_start(sctx, &wp->base); + sctx->flags |= SCREEN_WRITE_INPUT; screen_write_reset(sctx); screen_write_stop(sctx); } @@ -1064,6 +1065,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len) screen_write_start_pane(sctx, wp, &wp->base); else screen_write_start(sctx, &wp->base); + sctx->flags |= SCREEN_WRITE_INPUT; log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id, ictx->state->name, len, (int)len, buf); @@ -1083,6 +1085,7 @@ input_parse_screen(struct input_ctx *ictx, struct screen *s, return; screen_write_start_callback(sctx, s, cb, arg); + sctx->flags |= SCREEN_WRITE_INPUT; input_parse(ictx, buf, len); screen_write_stop(sctx); } diff --git a/screen-write.c b/screen-write.c index b37dd6283..8fda18fa1 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.299 2026/10/05 08:41:23 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.300 2026/10/12 18:47:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2564,8 +2564,9 @@ screen_write_collect_insert_clear(struct screen_write_ctx *ctx, u_int px, * background so the columns it covered do not lose their colour. */ static void -screen_write_clear_cell(struct grid *gd, u_int px, u_int py) +screen_write_clear_cell(struct screen_write_ctx *ctx, u_int px, u_int py) { + struct grid *gd = ctx->s->grid; struct grid_cell gc; int bg; @@ -2630,7 +2631,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, xx, s->cy, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; - screen_write_clear_cell(s->grid, xx, s->cy); + screen_write_clear_cell(ctx, xx, s->cy); log_debug("%s: padding erased (before) at %u (cx %u)", __func__, xx, s->cx); } @@ -2639,7 +2640,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, 0, s->cy, &gc); if (gc.data.width > 1 || (gc.flags & GRID_FLAG_PADDING)) { - screen_write_clear_cell(s->grid, xx, s->cy); + screen_write_clear_cell(ctx, xx, s->cy); log_debug("%s: padding erased (before) at %u " "(cx %u)", __func__, xx, s->cx); } @@ -2658,7 +2659,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, xx, s->cy, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; - screen_write_clear_cell(s->grid, xx, s->cy); + screen_write_clear_cell(ctx, xx, s->cy); log_debug("%s: padding erased (after) at %u (cx %u)", __func__, xx, s->cx); } @@ -3085,12 +3086,12 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc, if (~tmp_gc.flags & GRID_FLAG_PADDING) break; log_debug("%s: padding at %u,%u", __func__, xx, s->cy); - screen_write_clear_cell(gd, xx, s->cy); + screen_write_clear_cell(ctx, xx, s->cy); } /* Overwrite the character at the start of this padding. */ log_debug("%s: character at %u,%u", __func__, xx, s->cy); - screen_write_clear_cell(gd, xx, s->cy); + screen_write_clear_cell(ctx, xx, s->cy); done = 1; } @@ -3108,7 +3109,7 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc, break; log_debug("%s: overwrite at %u,%u", __func__, xx, s->cy); - screen_write_clear_cell(gd, xx, s->cy); + screen_write_clear_cell(ctx, xx, s->cy); done = 1; } } diff --git a/tmux.h b/tmux.h index b74ac1889..8011b26e7 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1457 2026/10/12 12:35:03 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1458 2026/10/12 18:47:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1101,6 +1101,7 @@ struct screen_write_ctx { #define SCREEN_WRITE_SYNC 0x1 #define SCREEN_WRITE_OBSCURED 0x2 #define SCREEN_WRITE_CHECKED_IF_OBSCURED 0x4 +#define SCREEN_WRITE_INPUT 0x8 screen_write_init_ctx_cb init_ctx_cb; void *arg; From 657fe07a89d24b55b45ef1c8814fbf10e68ca094 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 13 Oct 2026 07:12:54 +0100 Subject: [PATCH 07/12] Accidentally added this test too before the fix. --- regress/input-combine-width.sh | 88 ---------------------------------- 1 file changed, 88 deletions(-) delete mode 100644 regress/input-combine-width.sh diff --git a/regress/input-combine-width.sh b/regress/input-combine-width.sh deleted file mode 100644 index 4584d6b71..000000000 --- a/regress/input-combine-width.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/sh - -. ./input-common.inc - -# Exercise expansion by VS16, a skin tone modifier and a regional indicator. -for kind in variation modifier flag; do - case "$kind" in - variation) - combine_seq='\342\234\224\357\270\217' - text='βœ”οΈ' - ;; - modifier) - combine_seq='\342\230\235\360\237\217\273' - text='☝🏻' - ;; - flag) - combine_seq='\360\237\207\254\360\237\207\247' - text='πŸ‡¬πŸ‡§' - ;; - esac - - start_pane "$kind-overwrite" 10 3 "A\347\225\214BC\r${combine_seq}X" - check_capture "$kind-overwrite" "${text}XBC" - check_cursor "$kind-overwrite" '3,0' - check_raw_matches "$kind-overwrite" \ - "C 0,0 data=\(2,[0-9]+,$text\) flags=NONE\[0\]" \ - 'C 0,1 .*flags=PADDING\[[0-9a-f]+\]' \ - 'C 0,2 data=\(1,1,X\) flags=NONE\[0\]' - - start_pane "$kind-edge" 5 3 "abcd${combine_seq}X" - check_capture "$kind-edge" "abcd -${text}X" - check_cursor "$kind-edge" '3,1' - check_raw_matches "$kind-edge" \ - 'L 0 \(0\) flags=.*WRAPPED' \ - 'C 0,4 data=\(1,1, \)' \ - "C 1,0 data=\(2,[0-9]+,$text\) flags=NONE\[0\]" \ - 'C 1,1 .*flags=PADDING\[[0-9a-f]+\]' \ - 'C 1,2 data=\(1,1,X\) flags=NONE\[0\]' - - start_pane "$kind-fit" 5 3 "abc${combine_seq}X" - check_capture "$kind-fit" "abc${text} -X" - check_cursor "$kind-fit" '1,1' - - start_pane "$kind-scroll" 5 2 "11111\r\nabcd${combine_seq}X" - check_capture "$kind-scroll" "abcd -${text}X" - check_cursor "$kind-scroll" '3,1' - - start_pane "$kind-region" 5 4 \ - "top\033[2;1Hold\033[4;1Hend\033[2;3r\033[3;1Habcd${combine_seq}X" - check_capture "$kind-region" "top -abcd -${text}X -end" - check_cursor "$kind-region" '3,2' - - start_pane "$kind-insert" 10 3 "ABCDE\r\033[4h${combine_seq}X" - check_capture "$kind-insert" "${text}XABCDE" - check_cursor "$kind-insert" '3,0' - - start_pane "$kind-insert-edge" 5 3 \ - "\033[2;1HABCDE\033[1;1Habcd\033[4h${combine_seq}X" - check_capture "$kind-insert-edge" "abcd -${text}XAB" - check_cursor "$kind-insert-edge" '3,1' - - start_pane "$kind-nowrap" 5 3 "\033[?7labc${combine_seq}" - check_capture "$kind-nowrap" "abc${text}" - check_cursor "$kind-nowrap" '4,0' - check_raw_matches "$kind-nowrap" \ - "C 0,3 data=\(2,[0-9]+,$text\) flags=NONE\[0\]" \ - 'C 0,4 .*flags=PADDING\[[0-9a-f]+\]' -done - -# A two-column cluster cannot fit in a one-column pane. Keep its narrow base. -start_pane one-column 1 3 '\342\234\224\357\270\217' -check_capture one-column 'βœ”' -check_cursor one-column '1,0' -check_raw_no_matches one-column 'data=\(2,' - -# Without wrapping, even a wide cell filling the pane leaves the cursor inside. -start_pane two-columns-nowrap 2 3 '\033[?7l\342\234\224\357\270\217' -check_capture two-columns-nowrap 'βœ”οΈ' -check_cursor two-columns-nowrap '1,0' - -exit $exit_status From 80fbab8ae8746b2c7d9be2616545b333f6f5d369 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 13 Oct 2026 08:20:29 +0100 Subject: [PATCH 08/12] Fix test on macOS. --- regress/client-exit-resize.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/regress/client-exit-resize.sh b/regress/client-exit-resize.sh index a1dfab023..20b9f53a9 100755 --- a/regress/client-exit-resize.sh +++ b/regress/client-exit-resize.sh @@ -59,7 +59,13 @@ os.kill(client, signal.SIGTERM) size(master, 23, 61, 0, 0) os.kill(client, signal.SIGWINCH) os.kill(client, signal.SIGCONT) +# macOS drains the controlling terminal when its session leader exits. +# Keep reading output so the client can finish exiting. for i in range(200): + try: + os.read(master, 65536) + except OSError: + pass if os.waitpid(client, os.WNOHANG)[0] != 0: break time.sleep(0.05) From fca791a6f656e918c53aeda4196ef8820f169179 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 13 Oct 2026 11:22:15 +0100 Subject: [PATCH 09/12] OSC 133 cmdline tests. --- regress/format-variables.sh | 1 + regress/input-osc133.sh | 142 ++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 regress/input-osc133.sh diff --git a/regress/format-variables.sh b/regress/format-variables.sh index dced1a711..f37c42573 100644 --- a/regress/format-variables.sh +++ b/regress/format-variables.sh @@ -109,6 +109,7 @@ pane_bg pane_bottom pane_command_duration pane_command_end_time +pane_command_line pane_command_running pane_command_start_time pane_command_status diff --git a/regress/input-osc133.sh b/regress/input-osc133.sh new file mode 100644 index 000000000..c1a425b17 --- /dev/null +++ b/regress/input-osc133.sh @@ -0,0 +1,142 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +trap '$TMUX kill-server 2>/dev/null' 0 +trap 'exit 1' 1 2 15 + +check() +{ + format=$1 + expected=$2 + actual=$($TMUX -u display-message -p -t "$pane" "$format") || exit 1 + if [ "$actual" != "$expected" ]; then + printf 'format: %s\nexpected: %s\nactual: %s\n' \ + "$format" "$expected" "$actual" + exit 1 + fi +} + +emit() +{ + printf '\033]133;%s\033\\' "$1" | + $TMUX display-message -I -t "$pane" || exit 1 +} + +check_command() +{ + emit "C;$1" + check '#{pane_command_line}' "$2" + check '#{@started}' "$2" + check '#{pane_command_running}:#{pane_command_status}' '1:' + emit 'D;7' + check '#{pane_command_line}' "$2" + check '#{@finished}' "$2" + check '#{pane_command_running}:#{pane_command_status}' '0:7' +} + +$TMUX new-session -d -s test 'sleep 1000' || exit 1 +pane=$($TMUX new-window -d -P -F '#{pane_id}' '') || exit 1 +other=$($TMUX new-window -d -P -F '#{pane_id}' '') || exit 1 +$TMUX set-hook -g pane-command-started \ + 'set -gF @started "#{hook_command_line}"' || exit 1 +$TMUX set-hook -g pane-command-finished \ + 'set -gF @finished "#{hook_command_line}"' || exit 1 + +check '#{pane_command_line}:#{pane_command_running}' ':0' +check_command 'cmdline_url=sleep%201' 'sleep 1' + +# Prompt markers leave the most recent command available. +emit A +emit B +check '#{pane_command_line}' 'sleep 1' +$TMUX display-message -p -t "$other" '#{pane_command_line}' | + grep -qx '' || exit 1 + +# Unknown fields are ignored and decoded semicolons belong to the value. +check_command 'other=1;cmdline_url=echo%20one%3B%20echo%20two;other=2' \ + 'echo one; echo two' +check_command 'cmdline_url=echo%20a+b%2Bc%25%3d%3D' 'echo a+b+c%==' +check_command 'cmdline_url=echo%20%C3%A9%E4%B8%AD' 'echo Γ©δΈ­' +check_command 'cmdline_url=echo%20%2520' 'echo %20' + +# Use the same cleanup as titles for control characters and format jobs. +check_command 'cmdline_url=echo%20%23%28date%29' 'echo _(date)' +check_command 'cmdline_url=echo%20%5C' 'echo \\' + +# Multiline commands retain tabs and newlines as visible escapes, including +# in the event payloads used by hooks. +check_command 'cmdline_url=sleep%201%0Aecho%20done' 'sleep 1\necho done' +check_command 'cmdline_url=echo%09hello%0Aworld' 'echo\thello\nworld' + +# The name validator also permits these bytes before clean_name escapes them. +name=$(printf 'tab\tnewline\nname') +$TMUX rename-window -t "$other" "$name" || exit 1 +actual=$($TMUX -u display-message -p -t "$other" '#{window_name}') || exit 1 +[ "$actual" = 'tab\tnewline\nname' ] || exit 1 +$TMUX select-pane -t "$pane" -T "$name" || exit 1 +check '#{pane_title}' 'tab\tnewline\nname' + +# Every new start clears old metadata if it is missing or invalid. +emit C +check '#{pane_command_line}:#{@started}' ':' +for metadata in \ + 'cmdline_url=' \ + 'other=cmdline_url=stale' \ + 'xcmdline_url=stale' \ + 'cmdline_url=bad%' \ + 'cmdline_url=bad%2' \ + 'cmdline_url=bad%GG' \ + 'cmdline_url=bad%00tail' \ + 'cmdline_url=bad%0Dtail' \ + 'cmdline_url=bad%1Btail' \ + 'cmdline_url=bad%07tail' \ + 'cmdline_url=bad%08tail' \ + 'cmdline_url=bad%0Btail' \ + 'cmdline_url=bad%0Ctail' \ + 'cmdline_url=bad%7Ftail' \ + 'cmdline_url=bad%FF' \ + 'cmdline_url=bad%C3' \ + 'cmdline_url=bad%C0%AF' +do + check_command 'cmdline_url=previous' previous + check_command "$metadata" '' +done + +# The limit applies to decoded bytes, not the size of the OSC string. +decoded=$(printf '%5000s' '' | tr ' ' x) +check_command "cmdline_url=$decoded" "$decoded" +encoded=$(awk 'BEGIN { for (i = 0; i < 16384; i++) printf "%%78" }') +decoded=$(printf '%16384s' '' | tr ' ' x) +check_command "cmdline_url=$encoded" "$decoded" +check_command "cmdline_url=${encoded}%78" '' + +# Complete UTF-8 characters may reach the limit, but overlong lines are +# rejected in full rather than truncated, even through a character. +check_boundary() +{ + prefix=$(printf "%${1}s" '' | tr ' ' x) + check_command "cmdline_url=$prefix$2" "$prefix$3" + check_command "cmdline_url=${prefix}x$2" '' +} +check_boundary 16382 '%C3%A9' 'Γ©' +check_boundary 16381 '%E4%B8%AD' 'δΈ­' +check_boundary 16380 '%F0%9F%98%80' 'πŸ˜€' + +# Each hook gets a snapshot, even if multiple commands arrive in one read. +$TMUX set -g @log '' || exit 1 +$TMUX set-hook -g pane-command-started \ + 'set -gF @log "#{@log}|start:#{hook_command_line}"' || exit 1 +$TMUX set-hook -g pane-command-finished \ + 'set -gF @log "#{@log}|end:#{hook_command_line}:#{hook_command_status}"' || exit 1 +printf '\033]133;C;cmdline_url=first\007\033]133;D;0\007\033]133;C;cmdline_url=second\007\033]133;D;1\007\033]133;C\007\033]133;D;2\007' | + $TMUX display-message -I -t "$pane" || exit 1 +check '#{@log}' '|start:first|end:first:0|start:second|end:second:1|start:|end::2' +check '#{pane_command_line}' '' + +exit 0 From 9026b1d1974378b699fb51a200b96c64ca4924f1 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 13 Oct 2026 07:56:00 +0000 Subject: [PATCH 10/12] Complete layout names and options again, GitHub issue 5731 from intertermux at gmail dot com. --- prompt.c | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/prompt.c b/prompt.c index 300b278d8..39261e27c 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: prompt.c,v 1.7 2026/09/09 07:53:03 nicm Exp $ */ +/* $OpenBSD: prompt.c,v 1.8 2026/10/13 07:56:00 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1555,14 +1555,25 @@ prompt_complete_add(char ***list, u_int *size, const char *s) /* Build completion list. */ static char ** -prompt_complete_commands(u_int *size, const char *s) +prompt_complete_commands(u_int *size, const char *s, int at_start) { - char **list = NULL, *tmp; - const char *value, *cp; - const struct cmd_entry **cmdent; - size_t slen = strlen(s), valuelen; - struct options_entry *o; - struct options_array_item *a; + char **list = NULL, *tmp; + const char **layout, *value, *cp; + const struct cmd_entry **cmdent; + const struct options_table_entry *oe; + size_t slen = strlen(s), valuelen; + struct options_entry *o; + struct options_array_item *a; + const char *layouts[] = { + "even-horizontal", + "even-vertical", + "main-horizontal", + "main-horizontal-mirrored", + "main-vertical", + "main-vertical-mirrored", + "tiled", + NULL + }; *size = 0; for (cmdent = cmd_table; *cmdent != NULL; cmdent++) { @@ -1588,6 +1599,16 @@ prompt_complete_commands(u_int *size, const char *s) a = options_array_next(a); } } + if (at_start) + return (list); + for (oe = options_table; oe->name != NULL; oe++) { + if (strncmp(oe->name, s, slen) == 0) + prompt_complete_add(&list, size, oe->name); + } + for (layout = layouts; *layout != NULL; layout++) { + if (strncmp(*layout, s, slen) == 0) + prompt_complete_add(&list, size, *layout); + } return (list); } @@ -1671,11 +1692,10 @@ prompt_complete(struct prompt *pr, const char *word, u_int offset) char **list = NULL, *out = NULL; u_int size = 0, i; - if (pr->type != PROMPT_TYPE_COMMAND || offset != 0 || - *word == '\0') + if (pr->type != PROMPT_TYPE_COMMAND || *word == '\0') return (NULL); - list = prompt_complete_commands(&size, word); + list = prompt_complete_commands(&size, word, offset == 0); if (size == 0) { free(list); return (NULL); From baf8960837fbe2baa95de6c42823cee15a46882c Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 13 Oct 2026 10:21:58 +0000 Subject: [PATCH 11/12] Add format variables for the cmdline property to OSC 133, GitHub issue 5711. --- format.c | 16 +++++++++++++++- input.c | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- tmux.1 | 6 ++++-- tmux.h | 3 ++- utf8.c | 4 ++-- window.c | 3 ++- 6 files changed, 76 insertions(+), 8 deletions(-) diff --git a/format.c b/format.c index 361ae20bf..e391323f4 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.427 2026/10/12 12:22:42 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.428 2026/10/13 10:21:58 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2340,6 +2340,17 @@ format_cb_pane_last_prompt_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_command_line. */ +static void * +format_cb_pane_command_line(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL && wp->cmd_line != NULL) + return (xstrdup(wp->cmd_line)); + return (NULL); +} + /* Callback for pane_command_start_time. */ static void * format_cb_pane_command_start_time(struct format_tree *ft) @@ -3825,6 +3836,9 @@ static const struct format_table_entry format_table[] = { { "pane_command_end_time", FORMAT_TABLE_TIME, format_cb_pane_command_end_time }, + { "pane_command_line", FORMAT_TABLE_STRING, + format_cb_pane_command_line + }, { "pane_command_running", FORMAT_TABLE_STRING, format_cb_pane_command_running }, diff --git a/input.c b/input.c index 8ab63f5f6..062945ec0 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.275 2026/10/12 18:47:52 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.276 2026/10/13 10:21:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3186,6 +3186,52 @@ input_osc_133_exit_status(const char *p) return (status); } +/* Parse and clean the URL-encoded command line from OSC 133 C. */ +static char * +input_osc_133_command_line(const char *p) +{ + const char *end; + char line[16385], h[3] = { 0 }; + size_t size = 0; + u_char ch; + + if (p[1] != ';') + return (NULL); + p += 2; + for (;;) { + end = strchr(p, ';'); + if (end == NULL) + end = p + strlen(p); + if (end - p >= 12 && strncmp(p, "cmdline_url=", 12) == 0) + break; + if (*end == '\0') + return (NULL); + p = end + 1; + } + p += 12; + while (p < end) { + if (size == sizeof line - 1) + return (NULL); + ch = *p++; + if (ch == '%') { + if (end - p < 2 || + !isxdigit((u_char)p[0]) || + !isxdigit((u_char)p[1])) + return (NULL); + h[0] = p[0]; + h[1] = p[1]; + + ch = strtoul(h, NULL, 16); + if (ch == '\0') + return (NULL); + p += 2; + } + line[size++] = ch; + } + line[size] = '\0'; + return (clean_name(line, 1)); +} + /* Fire an OSC 133 command event. */ static void input_fire_command_event(struct window_pane *wp, const char *name) @@ -3205,6 +3251,8 @@ input_fire_command_event(struct window_pane *wp, const char *name) event_payload_set_window(ep, "window", wp->window); event_payload_set_pane(ep, "pane", wp); + if (wp->cmd_line != NULL) + event_payload_set_string(ep, "command_line", "%s", wp->cmd_line); if (wp->cmd_status != -1) event_payload_set_int(ep, "command_status", wp->cmd_status); if (tstart != 0) @@ -3276,6 +3324,8 @@ input_osc_133(struct input_ctx *ictx, const char *p) gl->osc133_data.out_start_col = s->cx; } if (wp != NULL) { + free(wp->cmd_line); + wp->cmd_line = input_osc_133_command_line(p); wp->cmd_start_time = time(NULL); wp->cmd_end_time = 0; wp->flags |= PANE_CMDRUNNING; diff --git a/tmux.1 b/tmux.1 index b19c2bb16..ee852c221 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1184 2026/10/12 12:35:03 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1185 2026/10/13 10:21:58 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: October 12 2026 $ +.Dd $Mdocdate: October 13 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -7445,6 +7445,7 @@ The following variables are available, where appropriate: .It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "hook" Ta "" Ta "Name of running hook, if any" .It Li "hook_client" Ta "" Ta "Name of client where hook was run, if any" +.It Li "hook_command_line" Ta "" Ta "Command line reported for an OSC 133 command hook, if any" .It Li "hook_fire_count" Ta "" Ta "Number of times hook has fired" .It Li "hook_fire_time" Ta "" Ta "Time hook last fired" .It Li "hook_last" Ta "" Ta "Previous value for a monitor hook" @@ -7513,6 +7514,7 @@ The following variables are available, where appropriate: .It Li "pane_bottom" Ta "" Ta "Bottom of pane" .It Li "pane_command_duration" Ta "" Ta "Current or most recent OSC 133 command duration in seconds" .It Li "pane_command_end_time" Ta "" Ta "Time most recent OSC 133 command ended" +.It Li "pane_command_line" Ta "" Ta "Current or most recent OSC 133 command line, if reported" .It Li "pane_command_running" Ta "" Ta "1 if an OSC 133 command is running" .It Li "pane_command_start_time" Ta "" Ta "Time current or most recent OSC 133 command started" .It Li "pane_command_status" Ta "" Ta "Exit status from most recent OSC 133 command" diff --git a/tmux.h b/tmux.h index 8011b26e7..aff0779ee 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1458 2026/10/12 18:47:52 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1459 2026/10/13 10:21:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1355,6 +1355,7 @@ struct window_pane { time_t cmd_start_time; time_t cmd_end_time; int cmd_status; + char *cmd_line; int fd; struct bufferevent *event; diff --git a/utf8.c b/utf8.c index 569b5c02a..607a740ff 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD: utf8.c,v 1.74 2026/10/05 08:32:24 nicm Exp $ */ +/* $OpenBSD: utf8.c,v 1.75 2026/10/13 10:21:58 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -822,7 +822,7 @@ utf8_isvalid(const char *s) continue; return (0); } - if (*s < 0x20 || *s > 0x7e) + if (*s != '\t' && *s != '\n' && (*s < 0x20 || *s > 0x7e)) return (0); s++; } diff --git a/window.c b/window.c index 0b280f19a..2d0e7ff0e 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.385 2026/10/08 07:06:26 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.386 2026/10/13 10:21:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1622,6 +1622,7 @@ window_pane_free(struct window_pane *wp) log_debug("pane %%%u freed (%d references)", wp->id, wp->references); free(wp->searchstr); + free(wp->cmd_line); screen_free(&wp->status_screen); screen_free(&wp->base); free(wp->r.ranges); From 2c64f131a9745be31693d6d8b968d20779d8594b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 13 Oct 2026 21:05:08 +0100 Subject: [PATCH 12/12] Some more image redraw bits. --- grid-view.c | 4 ++ image-grid.c | 79 ++++++++++++++++++++++++++++++++++ screen-write.c | 113 ++++++++++++++++++++++++++++++++++++++++++++++++- tmux-image.h | 8 ++++ 4 files changed, 202 insertions(+), 2 deletions(-) diff --git a/grid-view.c b/grid-view.c index 5659d5763..8c9794114 100644 --- a/grid-view.c +++ b/grid-view.c @@ -74,6 +74,10 @@ grid_view_clear_history(struct grid *gd, u_int bg) gl = grid_get_line(gd, grid_view_y(gd, yy)); if (gl->cellused != 0) last = yy + 1; +#ifdef ENABLE_IMAGES + else if (image_grid_line_has_images(gl)) + last = yy + 1; +#endif } if (last == 0) { grid_view_clear(gd, 0, 0, gd->sx, gd->sy, bg); diff --git a/image-grid.c b/image-grid.c index 3c87505d2..0873e9170 100644 --- a/image-grid.c +++ b/image-grid.c @@ -380,3 +380,82 @@ image_grid_line_has_images(struct grid_line *gl) { return (gl->images != NULL && !TAILQ_EMPTY(&gl->images->spans)); } + +/* Check whether a grid area has any image spans. */ +int +image_grid_area_has_images(struct grid *gd, u_int x, u_int y, u_int sx, + u_int sy) +{ + struct image_line *line; + struct image_span *span; + u_int row, end; + + if (gd->images == NULL) + return (0); + if (sx == 0 || sy == 0 || y >= gd->hsize + gd->sy) + return (0); + + end = x + sx; + if (end < x) + end = UINT_MAX; + if (sy > gd->hsize + gd->sy - y) + sy = gd->hsize + gd->sy - y; + + for (row = y; row < y + sy; row++) { + line = gd->linedata[row].images; + if (line == NULL) + continue; + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (span->x < end && span->x + span->sx > x) + return (1); + } + } + return (0); +} + +/* Record window damage when an area containing images changes. */ +void +image_redraw_area(struct screen_write_ctx *ctx, u_int x, u_int y, u_int sx, + u_int sy) +{ + struct window_pane *wp = ctx->wp; + struct grid *gd = ctx->s->grid; + + if (wp == NULL) + return; + if (!image_grid_area_has_images(gd, x, gd->hsize + y, sx, sy)) + return; + redraw_damage_window(wp->window, wp->xoff + x, wp->yoff + y, sx, sy); +} + +/* Record window damage for all visible images. */ +void +image_redraw_all(struct screen_write_ctx *ctx) +{ + struct screen *s = ctx->s; + u_int sx = screen_size_x(s), sy = screen_size_y(s); + + image_redraw_area(ctx, 0, 0, sx, sy); +} + +/* Record window damage for images in the scroll region. */ +void +image_redraw_scroll(struct screen_write_ctx *ctx) +{ + struct screen *s = ctx->s; + u_int sx = screen_size_x(s), sy = s->rlower - s->rupper + 1; + + image_redraw_area(ctx, 0, s->rupper, sx, sy); +} + +/* Redraw images under changed cells. */ +void +image_damage_screen(struct screen_write_ctx *ctx, u_int x, u_int y, u_int sx, + u_int sy) +{ + struct screen *s = ctx->s; + + image_redraw_area(ctx, x, y, sx, sy); + if (ctx->flags & SCREEN_WRITE_INPUT) + image_grid_remove(s->grid, x, s->grid->hsize + y, sx, sy); +} diff --git a/screen-write.c b/screen-write.c index 8fda18fa1..08e76cedb 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1471,6 +1471,10 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) memcpy(&gc, &grid_default_cell, sizeof gc); utf8_set(&gc.data, 'E'); +#ifdef ENABLE_IMAGES + image_damage_screen(ctx, 0, 0, screen_size_x(s), screen_size_y(s)); +#endif + for (yy = 0; yy < screen_size_y(s); yy++) { for (xx = 0; xx < screen_size_x(s); xx++) grid_view_set_cell(s->grid, xx, yy, &gc); @@ -1516,6 +1520,10 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) screen_write_initctx(ctx, &ttyctx, 0, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); +#endif + grid_view_insert_cells(s->grid, s->cx, s->cy, nx, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1552,6 +1560,10 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) screen_write_initctx(ctx, &ttyctx, 0, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); +#endif + grid_view_delete_cells(s->grid, s->cx, s->cy, nx, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1588,6 +1600,10 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) screen_write_initctx(ctx, &ttyctx, 0, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, s->cx, s->cy, nx, 1); +#endif + grid_view_clear(s->grid, s->cx, s->cy, nx, 1, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1624,6 +1640,10 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg) screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy); +#endif + grid_view_insert_lines(gd, s->cy, ny, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1648,6 +1668,11 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg) screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, s->cy, screen_size_x(s), + s->rlower + 1 - s->cy); +#endif + if (s->cy < s->rupper || s->cy > s->rlower) grid_view_insert_lines(gd, s->cy, ny, bg); else @@ -1687,6 +1712,10 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy); +#endif + grid_view_delete_lines(gd, s->cy, ny, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1713,6 +1742,10 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, s->cy, screen_size_x(s), ry); +#endif + if (s->cy < s->rupper || s->cy > s->rlower) grid_view_delete_lines(gd, s->cy, ny, bg); else @@ -1743,8 +1776,18 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg) u_int flags; gl = grid_get_line(s->grid, s->grid->hsize + s->cy); - if (gl->cellsize == 0 && COLOUR_DEFAULT(bg)) + if (gl->cellsize == 0 && COLOUR_DEFAULT(bg)) { +#ifdef ENABLE_IMAGES + if (!image_grid_line_has_images(gl)) + return; +#else return; +#endif + } + +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, s->cy, sx, 1); +#endif flags = gl->flags & GRID_LINE_OSC133_FLAGS; memcpy(&od, &gl->osc133_data, sizeof od); @@ -1777,8 +1820,21 @@ screen_write_clearendofline(struct screen_write_ctx *ctx, u_int bg) } gl = grid_get_line(s->grid, s->grid->hsize + s->cy); - if (s->cx > sx - 1 || (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg))) + if (s->cx > sx - 1) return; + if (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg)) { +#ifdef ENABLE_IMAGES + if (!image_grid_area_has_images(s->grid, s->cx, + s->grid->hsize + s->cy, sx - s->cx, 1)) + return; +#else + return; +#endif + } + +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, s->cx, s->cy, sx - s->cx, 1); +#endif grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg); @@ -1802,6 +1858,10 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg) return; } +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, s->cy, s->cx + 1, 1); +#endif + if (s->cx > sx - 1) grid_view_clear(s->grid, 0, s->cy, sx, 1, bg); else @@ -1851,6 +1911,10 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg) return; } +#ifdef ENABLE_IMAGES + image_redraw_scroll(ctx); +#endif + grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1918,6 +1982,10 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg) return; } +#ifdef ENABLE_IMAGES + image_redraw_scroll(ctx); +#endif + grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg); screen_write_collect_scroll(ctx, bg); ctx->scrolled++; @@ -1941,6 +2009,10 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg) ctx->bg = bg; } +#ifdef ENABLE_IMAGES + image_redraw_scroll(ctx); +#endif + for (i = 0; i < lines; i++) { grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg); screen_write_collect_scroll(ctx, bg); @@ -1965,6 +2037,10 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg) else if (lines > s->rlower - s->rupper + 1) lines = s->rlower - s->rupper + 1; +#ifdef ENABLE_IMAGES + image_redraw_scroll(ctx); +#endif + for (i = 0; i < lines; i++) grid_view_scroll_region_down(gd, s->rupper, s->rlower, bg); @@ -2004,6 +2080,12 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + if (s->cx < sx) + image_redraw_area(ctx, s->cx, s->cy, sx - s->cx, 1); + image_redraw_area(ctx, 0, s->cy + 1, sx, sy - (s->cy + 1)); +#endif + /* Scroll into history if it is enabled and clearing entire screen. */ if (s->cx == 0 && s->cy == 0 && @@ -2080,6 +2162,11 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_area(ctx, 0, 0, sx, s->cy); + image_redraw_area(ctx, 0, s->cy, s->cx + 1, 1); +#endif + if (s->cy > 0) grid_view_clear(s->grid, 0, 0, sx, s->cy, bg); if (s->cx > sx - 1) @@ -2148,6 +2235,10 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; +#ifdef ENABLE_IMAGES + image_redraw_all(ctx); +#endif + /* Scroll into history if it is enabled. */ if ((s->grid->flags & GRID_HISTORY) && ctx->wp != NULL && @@ -2573,6 +2664,10 @@ screen_write_clear_cell(struct screen_write_ctx *ctx, u_int px, u_int py) grid_view_get_cell(gd, px, py, &gc); bg = gc.bg; +#ifdef ENABLE_IMAGES + image_damage_screen(ctx, px, py, 1, 1); +#endif + memcpy(&gc, &grid_default_cell, sizeof gc); gc.bg = bg; grid_view_set_cell(gd, px, py, &gc); @@ -2649,6 +2744,10 @@ screen_write_collect_end(struct screen_write_ctx *ctx) } } +#ifdef ENABLE_IMAGES + image_damage_screen(ctx, s->cx, s->cy, ci->used, 1); +#endif + grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, ci->used); if (bnx != 0) @@ -2781,6 +2880,11 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) return; screen_write_initctx(ctx, &ttyctx, 0, 0); +#ifdef ENABLE_IMAGES + /* Mark images as damaged. */ + image_damage_screen(ctx, s->cx, s->cy, width, 1); +#endif + /* Handle overwriting of UTF-8 characters. */ gl = grid_get_line(s->grid, s->grid->hsize + s->cy); if (gl->flags & GRID_LINE_EXTENDED) { @@ -3012,6 +3116,11 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) } else force_wide = 0; +#ifdef ENABLE_IMAGES + /* Mark images as damaged. */ + image_damage_screen(ctx, cx - n, cy, n, 1); +#endif + /* Set the new cell. */ grid_view_set_cell(gd, cx - n, cy, &last); if (force_wide) diff --git a/tmux-image.h b/tmux-image.h index 61fb5598b..685b006bf 100644 --- a/tmux-image.h +++ b/tmux-image.h @@ -81,6 +81,14 @@ void image_grid_move_cells(struct grid *, u_int, u_int, u_int, void image_grid_duplicate_lines(struct grid *, u_int, struct grid *, u_int, u_int); int image_grid_line_has_images(struct grid_line *); +int image_grid_area_has_images(struct grid *, u_int, u_int, u_int, + u_int); +void image_redraw_area(struct screen_write_ctx *, u_int, u_int, + u_int, u_int); +void image_redraw_all(struct screen_write_ctx *); +void image_redraw_scroll(struct screen_write_ctx *); +void image_damage_screen(struct screen_write_ctx *, u_int, u_int, + u_int, u_int); /* image-sixel.c */ #define SIXEL_COLOUR_REGISTERS 1024