diff --git a/Makefile.am b/Makefile.am index 452fb07e6..eb587453d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -211,6 +211,7 @@ dist_tmux_SOURCES = \ style.c \ tmux-protocol.h \ tmux.c \ + tmux-image.h \ tmux.h \ tty-acs.c \ tty-draw.c \ @@ -252,7 +253,12 @@ endif # Enable image support. if ENABLE_IMAGES -dist_tmux_SOURCES += image.c image-sixel.c image-kitty.c image-fallback.c +dist_tmux_SOURCES += \ + image.c \ + image-grid.c \ + image-sixel.c \ + image-kitty.c \ + image-fallback.c endif # Add bits for fuzzing if enabled. diff --git a/format.c b/format.c index 2e4b4057d..cc97cbd51 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 @@ -2354,6 +2354,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) @@ -3843,6 +3854,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/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/grid.c b/grid.c index 123c6338e..155484967 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 @@ -701,6 +701,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]; @@ -1262,17 +1265,13 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, struct grid_line *dstl, *srcl; u_int yy; #ifdef ENABLE_IMAGES - u_int original_dy, original_sy; + u_int ody = dy, osy = sy; #endif if (dy + ny > dst->hsize + dst->sy) ny = dst->hsize + dst->sy - dy; if (sy + ny > src->hsize + src->sy) ny = src->hsize + src->sy - sy; -#ifdef ENABLE_IMAGES - original_dy = dy; - original_sy = sy; -#endif grid_free_lines(dst, dy, ny); for (yy = 0; yy < ny; yy++) { @@ -1303,7 +1302,7 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, dy++; } #ifdef ENABLE_IMAGES - image_grid_duplicate_lines(dst, original_dy, src, original_sy, ny); + image_grid_duplicate_lines(dst, ody, src, osy, ny); #endif } diff --git a/image-fallback.c b/image-fallback.c index 8454ebe52..428fb899a 100644 --- a/image-fallback.c +++ b/image-fallback.c @@ -252,27 +252,31 @@ fallback_block_key(enum image_glyph_detail detail, u_int mask) static u_char * fallback_make_shades(struct image *im, u_int levels) { - struct image_glyph_data *data = im->fallback_data; + struct image_glyph_data *data = image_get_fallback_data(im); const struct image_cell *cell; int *values, value, represented, error; size_t cells, index; u_int x, y, scan, level, minimum = 255, maximum = 0; + u_int sx, sy; int reverse; u_char *result; - if (data == NULL) - data = im->fallback_data = xcalloc(1, sizeof *data); + if (data == NULL) { + data = xcalloc(1, sizeof *data); + image_set_fallback_data(im, data); + } + image_get_size_in_cells(im, &sx, &sy); result = (levels == 5 ? data->shade5 : data->shade8); if (result != NULL) return (result); /* Collect cell brightness and the range used by this image. */ - cells = (size_t)im->sx * im->sy; + cells = (size_t)sx * sy; values = xcalloc(cells, sizeof *values); result = xcalloc(cells, 1); - for (y = 0; y < im->sy; y++) { - for (x = 0; x < im->sx; x++) { - index = (size_t)y * im->sx + x; + for (y = 0; y < sy; y++) { + for (x = 0; x < sx; x++) { + index = (size_t)y * sx + x; cell = image_get_cell(im, x, y); value = cell->whole.brightness; if ((u_int)value < minimum) @@ -291,11 +295,11 @@ fallback_make_shades(struct image *im, u_int levels) } /* Dither each row, alternating direction to distribute the error. */ - for (y = 0; y < im->sy; y++) { + for (y = 0; y < sy; y++) { reverse = (y & 1); - for (scan = 0; scan < im->sx; scan++) { - x = (reverse ? im->sx - scan - 1 : scan); - index = (size_t)y * im->sx + x; + 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; @@ -305,21 +309,21 @@ fallback_make_shades(struct image *im, u_int levels) result[index] = level; represented = level * 255 / (levels - 1); error = value - represented; - if (!reverse && x + 1 < im->sx) + if (!reverse && x + 1 < sx) values[index + 1] += error * 7 / 16; if (reverse && x != 0) values[index - 1] += error * 7 / 16; - if (y + 1 == im->sy) + if (y + 1 == sy) continue; if (!reverse && x != 0) - values[index + im->sx - 1] += error * 3 / 16; - if (reverse && x + 1 < im->sx) - values[index + im->sx + 1] += error * 3 / 16; - values[index + im->sx] += error * 5 / 16; - if (!reverse && x + 1 < im->sx) - values[index + im->sx + 1] += error / 16; + 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 + im->sx - 1] += error / 16; + values[index + sx - 1] += error / 16; } } free(values); @@ -447,7 +451,7 @@ fallback_get_cell(struct tty *tty, struct image *im, u_int x, u_int y, enum image_glyph_palette palette; enum image_glyph_detail detail; u_char *levels, key; - u_int level = 0, nlevels; + u_int level = 0, nlevels, sx, sy; memcpy(out, gc, sizeof *out); cell = image_get_cell(im, x, y); @@ -469,7 +473,8 @@ fallback_get_cell(struct tty *tty, struct image *im, u_int x, u_int y, if (detail == IMAGE_GLYPH_SHADE5) nlevels = 5; levels = fallback_make_shades(im, nlevels); - level = levels[(size_t)y * im->sx + x]; + image_get_size_in_cells(im, &sx, &sy); + level = levels[(size_t)y * sx + x]; key = (detail == IMAGE_GLYPH_SHADE5 ? shades[level] : bold_shades[level]); if (key == 0) @@ -492,7 +497,7 @@ fallback_get_cell(struct tty *tty, struct image *im, u_int x, u_int y, void fallback_free(struct image *im) { - struct image_glyph_data *data = im->fallback_data; + struct image_glyph_data *data = image_get_fallback_data(im); if (data == NULL) return; diff --git a/image-grid.c b/image-grid.c new file mode 100644 index 000000000..d67bbac4d --- /dev/null +++ b/image-grid.c @@ -0,0 +1,1491 @@ +/* $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. + */ + +#include + +#include +#include +#include + +#include "tmux.h" + +#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. */ +struct image_span { + u_int x; /* Owning grid column. */ + u_int sx; /* Number of image cells. */ + u_int source_x; /* Origin in image cells. */ + 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); + +/* Image spans attached to one grid line. */ +struct image_line { + struct image_spans spans; +}; + +enum image_input { + IMAGE_INPUT_ALL = -1, + IMAGE_INPUT_OVERWRITTEN = -2, + IMAGE_INPUT_SIXEL, + IMAGE_INPUT_KITTY +}; + +/* One logical image placement, shared by all of its row spans. */ +struct image_placement { + struct image_store *store; + struct image *image; + enum image_input input; + int placeholder; /* Uses Unicode placeholders. */ + u_int app_image_id; + u_int app_placement_id; + int32_t z; + uint64_t serial; /* Layer order. */ + struct image_spans spans; + TAILQ_ENTRY(image_placement) entry; +}; +TAILQ_HEAD(image_placements, image_placement); + +/* Placements belonging to one grid. */ +struct image_store { + uint64_t next_serial; + struct image_placements placements; +}; + +/* Spans saved while overlapping grid cells are moved. */ +struct image_move { + struct image_placement *placement; + u_int x; /* Destination grid column. */ + u_int sx; /* Number of image cells. */ + u_int source_x; /* Origin in image cells. */ + u_int source_y; +}; + +/* Pair source and destination placements during one grid copy. */ +struct image_placement_map { + struct image_placement *source; + struct image_placement *destination; +}; + +/* Placement mappings kept only for the duration of a grid copy. */ +struct image_copy_ctx { + struct grid *destination; + struct image_placement_map *maps; + size_t count; /* Placement mapping count. */ +}; + +struct image_backend { + const char *name; + int flags; + void (*draw_rect)(struct tty *, const struct image_rect *); + void (*free)(struct tty *, int); +}; + +static const struct image_backend image_backend_fallback = { + "fallback", IMAGE_BACKEND_SCROLLS, NULL, NULL +}; +static const struct image_backend image_backend_kitty = { + "kitty", + IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED, + kitty_draw_rect, kitty_free_output_state +}; +static const struct image_backend image_backend_sixel = { + "sixel", IMAGE_BACKEND_GRAPHICAL, + sixel_draw_rect, sixel_free_output +}; + +/* Find the image backend supported by a terminal. */ +static const struct image_backend * +image_tty_find_backend(struct tty *tty) +{ + if (tty->term != NULL && tty->term->flags & TERM_KITTYGRAPHICS) + 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. */ +int +image_tty_update(struct tty *tty) +{ + const struct image_backend *backend; + + backend = image_tty_find_backend(tty); + if (tty->image_backend == backend) + return (0); + + if (tty->image_backend != NULL && tty->image_backend->free != NULL) + tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED)); + tty->image_data = NULL; + tty->image_backend = backend; + log_debug("%s: %s image backend is %s", __func__, + tty->client->name, backend->name); + return (1); +} + +/* Prepare the image backend to replace a redraw area. */ +void +image_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, + u_int height) +{ + image_tty_update(tty); + if (tty->image_backend == &image_backend_kitty) + 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. */ +void +image_redraw_finish(struct tty *tty) +{ + if (tty->image_backend == &image_backend_kitty) + kitty_redraw_finish(tty); +} + +/* Write out any image output the backend is still holding back. */ +void +image_draw_flush(struct tty *tty) +{ + image_tty_update(tty); + if (tty->image_backend == &image_backend_sixel) + sixel_flush_output(tty); +} + +/* Return the flags for a terminal's image backend. */ +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 +image_tty_geometry_changed(struct tty *tty) +{ + image_tty_update(tty); + if (tty->image_backend->free != NULL) + tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED)); +} + +/* Free image backend state for a terminal. */ +void +image_tty_free(struct tty *tty, int send) +{ + if (tty->image_backend != NULL && tty->image_backend->free != NULL) + tty->image_backend->free(tty, send); + tty->image_backend = NULL; + tty->image_data = NULL; +} + +/* Return the ordering band for a placement. */ +static int +image_placement_band(const struct image_placement *placement) +{ + if (placement->input == IMAGE_INPUT_SIXEL) + return (1); + if (placement->z < 0) + return (0); + return (2); +} + +/* Compare two placements in logical drawing order. */ +static int +image_placement_cmp(const struct image_placement *a, + const struct image_placement *b) +{ + int aband, bband; + + aband = image_placement_band(a); + bband = image_placement_band(b); + if (aband != bband) + return (aband < bband ? -1 : 1); + if (a->input == IMAGE_INPUT_KITTY && a->z != b->z) + return (a->z < b->z ? -1 : 1); + if (a->input == IMAGE_INPUT_KITTY && + image_get_order_id(a->image) != image_get_order_id(b->image)) { + return (image_get_order_id(a->image) < + image_get_order_id(b->image) ? -1 : 1); + } + if (a->serial != b->serial) + return (a->serial < b->serial ? -1 : 1); + return (0); +} + +/* Allocate the image store for a grid when first needed. */ +static struct image_store * +image_store_get(struct grid *gd) +{ + struct image_store *store = gd->images; + + if (store == NULL) { + store = xcalloc(1, sizeof *store); + TAILQ_INIT(&store->placements); + gd->images = store; + } + return (store); +} + +/* Allocate the image span list for a grid line when first needed. */ +static struct image_line * +image_line_get(struct grid_line *gl) +{ + struct image_line *line = gl->images; + + if (line == NULL) { + line = xcalloc(1, sizeof *line); + TAILQ_INIT(&line->spans); + gl->images = line; + } + return (line); +} + +/* Create a logical image placement. */ +static struct image_placement * +image_placement_create(struct grid *gd, struct image *im, + enum image_input input, u_int app_image_id, u_int app_placement_id, + int32_t z) +{ + struct image_store *store = image_store_get(gd); + struct image_placement *placement; + + placement = xcalloc(1, sizeof *placement); + placement->store = store; + placement->image = im; + placement->input = input; + placement->app_image_id = app_image_id; + placement->app_placement_id = app_placement_id; + placement->z = z; + placement->serial = ++store->next_serial; + if (placement->serial == 0) + placement->serial = ++store->next_serial; + TAILQ_INIT(&placement->spans); + TAILQ_INSERT_TAIL(&store->placements, placement, entry); + image_ref(im); + return (placement); +} + +/* Free a placement which no longer has any spans. */ +static void +image_placement_free(struct image_placement *placement) +{ + if (!TAILQ_EMPTY(&placement->spans)) + fatalx("freeing image placement with spans"); + TAILQ_REMOVE(&placement->store->placements, placement, entry); + image_free(placement->image); + free(placement); +} + +/* Insert a span into both its line and placement lists. */ +static struct image_span * +image_span_add(struct image_line *line, struct image_placement *placement, + u_int x, u_int width, u_int source_x, u_int source_y) +{ + struct image_span *span, *at; + + if (width == 0) + return (NULL); + span = xcalloc(1, sizeof *span); + span->x = x; + span->sx = width; + span->source_x = source_x; + span->source_y = source_y; + span->line = line; + span->placement = placement; + TAILQ_FOREACH(at, &line->spans, line_entry) { + if (image_placement_cmp(placement, at->placement) < 0 || + (placement == at->placement && x < at->x)) { + TAILQ_INSERT_BEFORE(at, span, line_entry); + goto inserted; + } + } + TAILQ_INSERT_TAIL(&line->spans, span, line_entry); +inserted: + TAILQ_INSERT_TAIL(&placement->spans, span, placement_entry); + return (span); +} + +/* Unlink and free one span without pruning its placement. */ +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); +} + +/* Remove a cell range from one placement or the selected input type. */ +static void +image_line_remove(struct image_line *line, u_int x, u_int width, + enum image_input input, + struct image_placement *only) +{ + struct image_span *span, *next; + u_int end, span_end, right; + + if (line == NULL || width == 0) + return; + end = x + width; + if (end < x) + end = UINT_MAX; + TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) { + if (only != NULL && span->placement != only) + continue; + if (input == IMAGE_INPUT_OVERWRITTEN) { + /* Keep ordinary Kitty placements on text writes. */ + if (span->placement->input != IMAGE_INPUT_SIXEL && + !span->placement->placeholder) { + continue; + } + } else if (input != IMAGE_INPUT_ALL) { + if (span->placement->input != input) + continue; + } + span_end = span->x + span->sx; + 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. */ +static void +image_store_prune(struct image_store *store) +{ + struct image_placement *placement, *next; + + if (store == NULL) + return; + TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { + if (TAILQ_EMPTY(&placement->spans)) + image_placement_free(placement); + } +} + +/* Remove SIXEL and placeholder spans overwritten by text. */ +void +image_grid_remove_overwritten_spans(struct grid *gd, u_int x, u_int y, + u_int width, u_int height) +{ + u_int row; + + if (gd->images == NULL || width == 0 || height == 0) + return; + if (y >= gd->hsize + gd->sy) + return; + if (height > gd->hsize + gd->sy - y) + height = gd->hsize + gd->sy - y; + for (row = y; row < y + height; row++) { + image_line_remove(gd->linedata[row].images, x, width, + IMAGE_INPUT_OVERWRITTEN, NULL); + } + image_store_prune(gd->images); +} + +/* Free all image spans belonging to 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 the empty image store when a grid is destroyed. */ +void +image_grid_free(struct grid *gd) +{ + if (gd->images == NULL) + return; + image_store_prune(gd->images); + if (!TAILQ_EMPTY(&gd->images->placements)) + fatalx("freeing grid with image placements"); + free(gd->images); + gd->images = NULL; +} + +/* Move image spans with a range of grid 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 count = 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; + + /* Save source spans before removing the overlapping cell ranges. */ + 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, count + 1, sizeof *moves); + moves[count].placement = span->placement; + moves[count].x = dx + start - px; + moves[count].sx = span_end - start; + moves[count].source_x = span->source_x + start - span->x; + moves[count].source_y = span->source_y; + count++; + } + + /* Replace both ranges and restore spans at their new columns. */ + image_line_remove(line, px, nx, IMAGE_INPUT_ALL, NULL); + image_line_remove(line, dx, nx, IMAGE_INPUT_ALL, NULL); + for (i = 0; i < count; 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); +} + +/* Find or create the destination placement for a copied source placement. */ +static struct image_placement * +image_copy_placement(struct image_copy_ctx *ctx, + struct image_placement *source) +{ + struct image_placement *placement; + size_t i; + + for (i = 0; i < ctx->count; i++) { + if (ctx->maps[i].source == source) + return (ctx->maps[i].destination); + } + placement = image_placement_create(ctx->destination, source->image, + source->input, source->app_image_id, source->app_placement_id, + source->z); + placement->placeholder = source->placeholder; + ctx->maps = xreallocarray(ctx->maps, ctx->count + 1, + sizeof *ctx->maps); + ctx->maps[ctx->count].source = source; + ctx->maps[ctx->count].destination = placement; + ctx->count++; + return (placement); +} + +/* Duplicate image spans alongside a group of 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_ctx ctx = { .destination = dst }; + struct image_placement *placement; + struct image_line *source_line, *destination_line; + struct image_span *span; + u_int row; + + for (row = 0; row < ny; row++) { + source_line = src->linedata[sy + row].images; + if (source_line == NULL) + continue; + destination_line = image_line_get(&dst->linedata[dy + row]); + TAILQ_FOREACH(span, &source_line->spans, line_entry) { + placement = image_copy_placement(&ctx, span->placement); + image_span_add(destination_line, placement, span->x, + span->sx, span->source_x, span->source_y); + } + } + free(ctx.maps); +} + +/* Copy clipped image spans between grid areas. */ +void +image_grid_copy_area(struct grid *dst, u_int destination_x, + u_int destination_y, struct grid *src, u_int source_x, u_int source_y, + u_int sx, u_int sy) +{ + struct image_copy_ctx ctx = { .destination = dst }; + struct image_placement *placement; + struct image_line *source_line, *destination_line; + struct image_span *span; + u_int row, start, end, span_end; + + if (dst == src || sx == 0 || sy == 0) + return; + if (destination_y >= dst->hsize + dst->sy || + source_y >= src->hsize + src->sy) { + return; + } + if (sy > dst->hsize + dst->sy - destination_y) + sy = dst->hsize + dst->sy - destination_y; + if (sy > src->hsize + src->sy - source_y) + sy = src->hsize + src->sy - source_y; + end = source_x + sx; + if (end < source_x) + end = UINT_MAX; + + /* Copy clipped spans, sharing one destination placement per source. */ + for (row = 0; row < sy; row++) { + source_line = src->linedata[source_y + row].images; + if (source_line == NULL) + continue; + destination_line = image_line_get( + &dst->linedata[destination_y + row]); + TAILQ_FOREACH(span, &source_line->spans, line_entry) { + span_end = span->x + span->sx; + start = (span->x > source_x ? span->x : source_x); + if (start >= end || span_end <= source_x) + continue; + if (span_end > end) + span_end = end; + + placement = image_copy_placement(&ctx, span->placement); + image_span_add(destination_line, placement, + destination_x + start - source_x, span_end - start, + span->source_x + start - span->x, span->source_y); + } + } + free(ctx.maps); +} + +/* Return whether a grid line contains any image spans. */ +int +image_grid_line_has_images(struct grid_line *gl) +{ + return (gl->images != NULL && !TAILQ_EMPTY(&gl->images->spans)); +} + +/* Return whether a grid rectangle contains any image spans. */ +int +image_grid_area_has_images(struct grid *gd, u_int x, u_int y, u_int width, + u_int height) +{ + struct image_line *line; + struct image_span *span; + u_int row, end; + + if (gd->images == NULL || width == 0 || height == 0 || + y >= gd->hsize + gd->sy) { + return (0); + } + end = x + width; + if (end < x) + end = UINT_MAX; + if (height > gd->hsize + gd->sy - y) + height = gd->hsize + gd->sy - y; + for (row = y; row < y + height; 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); +} + +/* Find the resolved placeholder at one grid cell. */ +int +image_grid_get_placeholder(struct grid *gd, u_int x, u_int y, + struct kitty_placeholder *placeholder) +{ + struct image_line *line; + struct image_span *span, *found = NULL; + + if (y >= gd->hsize + gd->sy || + (line = gd->linedata[y].images) == NULL) { + return (0); + } + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (!span->placement->placeholder) + continue; + if (x < span->x) + continue; + if (x < span->x + span->sx) + found = span; + } + if (found == NULL) + return (0); + placeholder->image = found->placement->image; + placeholder->source_x = found->source_x + x - found->x; + placeholder->source_y = found->source_y; + placeholder->image_id = found->placement->app_image_id; + placeholder->placement_id = found->placement->app_placement_id; + placeholder->z = found->placement->z; + return (1); +} + +/* Add one Kitty Unicode-placeholder cell to an image placement. */ +void +image_place_cell_kitty(struct screen_write_ctx *ctx, struct image *im, + u_int x, u_int y, u_int source_x, u_int source_y, u_int image_id, + u_int placement_id, int32_t z) +{ + struct grid *gd = ctx->s->grid; + struct image_store *store = image_store_get(gd); + struct image_placement *placement = NULL, *candidate; + struct image_line *line; + struct image_span *span; + + /* Reuse a matching placeholder placement in this grid. */ + TAILQ_FOREACH_REVERSE(candidate, &store->placements, + image_placements, entry) { + if (!candidate->placeholder) + continue; + if (candidate->input == IMAGE_INPUT_KITTY && + candidate->image == im && + candidate->app_image_id == image_id && + candidate->app_placement_id == placement_id && + candidate->z == z) { + placement = candidate; + break; + } + } + if (placement == NULL) { + placement = image_placement_create(gd, im, IMAGE_INPUT_KITTY, + image_id, placement_id, z); + placement->placeholder = 1; + } + /* Extend an adjacent source run or add a new span for this cell. */ + line = image_line_get(&gd->linedata[gd->hsize + y]); + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (span->placement == placement && span->x + span->sx == x && + span->source_y == source_y && + span->source_x + span->sx == source_x) { + span->sx++; + image_redraw_area(ctx, x, y, 1, 1); + return; + } + } + image_span_add(line, placement, x, 1, source_x, source_y); + image_redraw_area(ctx, x, y, 1, 1); +} + +/* Return the image for a drawing rectangle. */ +struct image * +image_rect_get_image(const struct image_rect *rectangle) +{ + return (rectangle->image); +} + +/* Return the source and destination coordinates of a drawing rectangle. */ +void +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 *destination_x, u_int *destination_y) +{ + *source_x = rectangle->source_x; + *source_y = rectangle->source_y; + *width = rectangle->sx; + *height = rectangle->sy; + *destination_x = rectangle->destination_x; + *destination_y = rectangle->destination_y; +} + +/* Return the output z-index for a drawing rectangle. */ +int64_t +image_rect_get_z(const struct image_rect *rectangle) +{ + return (rectangle->z); +} + +/* Return one for a fallback cell, minus one to continue along an image line. */ +int +image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, + const struct grid_cell *gc, struct grid_cell *out) +{ + struct image_line *line; + struct image_span *span, *found = NULL; + struct image_placement *placement; + + if (image_backend_flags(tty) & IMAGE_BACKEND_GRAPHICAL || + y >= s->grid->sy) { + return (0); + } + line = s->grid->linedata[s->grid->hsize + y].images; + if (line == NULL) + return (0); + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (x >= span->x && x < span->x + span->sx) + found = span; + } + if (found == NULL) + return (-1); + placement = found->placement; + if (placement->input == IMAGE_INPUT_KITTY && placement->z < 0) { + if (!kitty_cell_is_placeholder(gc)) { + if (gc->data.size != 1) + return (-1); + if (gc->data.data[0] != ' ') + return (-1); + } + if (placement->z < IMAGE_Z_BELOW_BACKGROUND && + !COLOUR_DEFAULT(gc->bg)) { + return (-1); + } + } + fallback_get_cell(tty, placement->image, + found->source_x + x - found->x, found->source_y, gc, out); + return (1); +} + +/* Remove one placement and all of its spans. */ +static void +image_remove_placement(struct image_placement *placement) +{ + struct image_span *span, *next; + + TAILQ_FOREACH_SAFE(span, &placement->spans, placement_entry, next) + image_span_free(span); + image_placement_free(placement); +} + +/* Clear image placements with an internal image ID from a screen. */ +void +image_clear(struct screen_write_ctx *ctx, u_int id) +{ + struct image_store *store = ctx->s->grid->images; + struct image_placement *placement, *next; + + if (store == NULL) + return; + TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { + if (id != 0) { + if (image_get_source_id(placement->image) != id) { + if (placement->placeholder) + continue; + if (image_get_id(placement->image) != id) + continue; + } + } + image_remove_placement(placement); + } + if (ctx->wp != NULL) + ctx->wp->flags |= PANE_REDRAW; +} + +/* Return whether a grid still references an image or its source. */ +int +image_grid_has_image(struct grid *gd, u_int id) +{ + struct image_placement *placement; + + if (gd->images == NULL) + return (0); + TAILQ_FOREACH(placement, &gd->images->placements, entry) { + if (image_get_id(placement->image) == id) + return (1); + if (image_get_source_id(placement->image) == id) + return (1); + } + return (0); +} + +/* Return whether a placement intersects the selected screen cells. */ +static int +image_placement_intersects(struct grid *gd, struct image_placement *placement, + const struct kitty_parse_result *result) +{ + struct image_line *line; + struct image_span *span; + u_int x = result->x, y = result->y, row; + char how = result->delete; + int match_x = 0, match_y = 0; + + if (how >= 'A') { + if (how <= 'Z') + how += 'a' - 'A'; + } + switch (how) { + case 'p': case 'q': case 'c': + match_y = 1; + /* FALLTHROUGH */ + case 'x': + match_x = 1; + break; + case 'y': + match_y = 1; + break; + } + for (row = 0; row < gd->sy; row++) { + if (match_y) { + if (y == 0) + continue; + if (row != y - 1) + continue; + } + line = gd->linedata[gd->hsize + row].images; + if (line == NULL) + continue; + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (span->placement != placement) + continue; + if (span->x >= gd->sx) + continue; + if (match_x) { + if (x == 0) + continue; + if (x > gd->sx) + continue; + if (x - 1 < span->x) + continue; + if (x - 1 >= span->x + span->sx) + continue; + } + return (1); + } + } + return (0); +} + +/* Clear ordinary Kitty placements selected by a graphics delete command. */ +void +image_clear_kitty(struct screen_write_ctx *ctx, + const struct kitty_parse_result *result) +{ + struct grid *gd = ctx->s->grid; + struct image_store *store = gd->images; + struct image_placement *placement, *next; + struct kitty_parse_result selection = *result; + char how = result->delete; + int matched; + u_int placement_id = result->placement_id; + + if (store == NULL) + return; + if (how >= 'A') { + if (how <= 'Z') + how += 'a' - 'A'; + } + if (how == 'c') { + selection.x = ctx->s->cx + 1; + selection.y = ctx->s->cy + 1; + } + TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { + if (placement->input != IMAGE_INPUT_KITTY) + continue; + if (placement->placeholder) + continue; + matched = 0; + switch (how) { + case 'i': + if (placement->app_image_id != result->image_id) + break; + if (placement_id != 0) { + if (placement->app_placement_id != placement_id) + break; + } + matched = 1; + break; + case 'r': + if (placement->app_image_id < result->x) + break; + if (placement->app_image_id > result->y) + break; + matched = 1; + break; + case 'z': + if (placement->z == result->z) + matched = 1; + break; + case 'q': + if (placement->z != result->z) + break; + /* FALLTHROUGH */ + case 'a': case 'c': case 'p': case 'x': case 'y': + matched = image_placement_intersects(gd, placement, + &selection); + break; + } + if (matched) + image_remove_placement(placement); + } + if (ctx->wp != NULL) + ctx->wp->flags |= PANE_REDRAW; +} + +/* Record window damage for image layers in a screen area. */ +void +image_redraw_area(struct screen_write_ctx *ctx, u_int px, u_int py, u_int nx, + u_int ny) +{ + struct window_pane *wp = ctx->wp; + + if (wp == NULL) + return; + if (!image_grid_area_has_images(ctx->s->grid, px, + ctx->s->grid->hsize + py, nx, ny)) { + return; + } + redraw_damage_window(wp->window, wp->xoff + px, wp->yoff + py, nx, + ny); +} + +/* Redraw all image layers on a screen. */ +void +image_redraw_all(struct screen_write_ctx *ctx) +{ + image_redraw_area(ctx, 0, 0, screen_size_x(ctx->s), + screen_size_y(ctx->s)); +} + +/* Record image damage within the current scroll region. */ +void +image_redraw_scroll(struct screen_write_ctx *ctx) +{ + struct screen *s = ctx->s; + struct window_pane *wp = ctx->wp; + + if (wp == NULL) + return; + if (!image_grid_area_has_images(s->grid, 0, s->grid->hsize + s->rupper, + screen_size_x(s), s->rlower - s->rupper + 1)) { + return; + } + redraw_damage_window_scroll(wp->window, wp->xoff, + wp->yoff + s->rupper, screen_size_x(s), + s->rlower - s->rupper + 1); +} + +/* Draw a clipped part of one image span. */ +static void +image_draw_span(const struct image_backend *backend, struct tty *tty, + struct image_span *span, u_int start, u_int end, + u_int px, u_int atx, u_int aty) +{ + struct image_placement *placement = span->placement; + struct image_rect rectangle; + + rectangle.image = placement->image; + if (placement->input == IMAGE_INPUT_SIXEL) + rectangle.z = 0; + else if (placement->z >= 0) + rectangle.z = (int64_t)placement->z + 1; + else + rectangle.z = placement->z; + rectangle.source_x = span->source_x + start - span->x; + rectangle.source_y = span->source_y; + rectangle.sx = end - start; + rectangle.sy = 1; + rectangle.destination_x = atx + start - px; + rectangle.destination_y = aty; + backend->draw_rect(tty, &rectangle); +} + +/* Return whether a cell contains a glyph or text decoration. */ +static int +image_cell_has_text(struct grid *gd, u_int x, u_int y) +{ + struct grid_cell gc; + + grid_view_get_cell(gd, x, y, &gc); + if (kitty_cell_is_placeholder(&gc)) + return (0); + if (gc.data.size != 1 || gc.data.data[0] != ' ') + return (1); + return (gc.attr != 0); +} + +/* Draw a span's graphical image layers before or after its text. */ +void +image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, + u_int nx, u_int atx, u_int aty, int before) +{ + const struct image_backend *backend; + struct image_line *line; + struct image_span *span; + struct image_placement *placement; + u_int start, end, span_end, draw_end; + int blank_only; + + image_tty_update(tty); + backend = tty->image_backend; + if (~backend->flags & IMAGE_BACKEND_GRAPHICAL) + return; + if (py >= s->grid->sy) + return; + line = s->grid->linedata[s->grid->hsize + py].images; + if (line == NULL) + return; + end = px + nx; + TAILQ_FOREACH(span, &line->spans, line_entry) { + placement = span->placement; + blank_only = 0; + + /* Emulate Kitty's negative layers with SIXEL. */ + if (placement->input == IMAGE_INPUT_KITTY && + placement->z < 0) { + if (backend == &image_backend_sixel && + placement->z >= IMAGE_Z_BELOW_BACKGROUND) { + if (before) + continue; + blank_only = 1; + } else if (!before) + continue; + } else if (before) + continue; + 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; + if (!blank_only) { + image_draw_span(backend, tty, span, start, span_end, + px, atx, aty); + continue; + } + /* Draw below-text SIXEL layers only on cells without text. */ + while (start < span_end) { + while (start < span_end && + image_cell_has_text(s->grid, start, py)) + start++; + draw_end = start; + while (draw_end < span_end && + !image_cell_has_text(s->grid, draw_end, py)) + draw_end++; + if (start < draw_end) { + image_draw_span(backend, tty, span, start, + draw_end, px, atx, aty); + } + start = draw_end; + } + } +} + +/* Return whether an image cell contains at least one nontransparent pixel. */ +static int +image_cell_has_alpha(struct image *im, u_int x, u_int y) +{ + u_int px, py, sx, sy, xx, yy; + const u_char *pixels; + size_t stride; + + image_get_pixel_rect(im, x, y, 1, 1, &px, &py, &sx, &sy); + if (sx == 0 || sy == 0) + return (0); + pixels = image_get_pixels(im, &stride, NULL); + for (yy = py; yy < py + sy; yy++) { + for (xx = px; xx < px + sx; xx++) { + if (pixels[(size_t)yy * stride + + (size_t)xx * 4 + 3] != 0) { + return (1); + } + } + } + return (0); +} + +/* Return whether a new cell's pixels completely cover the old cell. */ +static int +image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old, + u_int ox, u_int oy) +{ + u_int npx, npy, nsx, nsy, opx, opy, osx, osy, xx, yy; + u_int ncw, nch, ocw, och, ncx, ncy, ocx, ocy; + const u_char *np, *op, *npixels, *opixels; + size_t nstride, ostride; + + image_get_canvas_size(new, &ncw, &nch); + image_get_canvas_size(old, &ocw, &och); + image_get_size_in_cells(new, &ncx, &ncy); + image_get_size_in_cells(old, &ocx, &ocy); + if (ncw / ncx != ocw / ocx || nch / ncy != och / ocy) { + return (0); + } + image_get_pixel_rect(new, nx, ny, 1, 1, &npx, &npy, &nsx, &nsy); + image_get_pixel_rect(old, ox, oy, 1, 1, &opx, &opy, &osx, &osy); + + npixels = image_get_pixels(new, &nstride, NULL); + opixels = image_get_pixels(old, &ostride, NULL); + for (yy = 0; yy < osy; yy++) { + op = opixels + (size_t)(opy + yy) * ostride + + (size_t)opx * 4; + if (yy < nsy) { + np = npixels + (size_t)(npy + yy) * nstride + + (size_t)npx * 4; + } + for (xx = 0; xx < osx; xx++) { + if (op[xx * 4 + 3] == 0) + continue; + if (yy >= nsy || xx >= nsx || np[xx * 4 + 3] != 255) + return (0); + } + } + return (1); +} + +/* Discard cells of older SIXEL placements fully covered by a new image. */ +static void +image_line_cover(struct image_line *line, struct image_placement *placement, + u_int x, u_int source_x, u_int source_y) +{ + struct image_span *span, *next; + struct image_placement *old; + + TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) { + old = span->placement; + if (old == placement || old->input != IMAGE_INPUT_SIXEL) + continue; + if (x < span->x || x >= span->x + span->sx) + continue; + if (!image_cell_covers(placement->image, source_x, source_y, + old->image, span->source_x + x - span->x, span->source_y)) { + continue; + } + image_line_remove(line, x, 1, IMAGE_INPUT_ALL, old); + } +} + +/* Add spans for one row of a placement between two source columns. */ +static void +image_extend_row(struct image_line *line, struct image_placement *placement, + u_int cx, u_int source_y, u_int old_end, u_int new_end) +{ + struct image *im = placement->image; + u_int x, run; + + for (x = old_end; x < new_end; x += run) { + if (!image_cell_has_alpha(im, x, source_y)) { + run = 1; + continue; + } + for (run = 1; x + run < new_end; run++) { + if (!image_cell_has_alpha(im, x + run, source_y)) + break; + } + image_span_add(line, placement, cx + x, run, x, source_y); + } +} + +/* Extend image placements to reveal clipped columns when a pane grows. */ +void +image_grid_resize_width(struct grid *gd, u_int new_sx) +{ + struct grid_line *gl; + struct image_line *line; + struct image_span *span; + struct image_placement *placement; + struct image_placement **seen = NULL; + size_t nseen, i; + u_int row, cx, end_x, avail, image_sx, image_sy; + u_int source_y; + int found; + + if (gd->images == NULL) + return; + for (row = 0; row < gd->hsize + gd->sy; row++) { + gl = &gd->linedata[row]; + line = gl->images; + if (line == NULL) + continue; + + /* Collect placements before modifying their spans. */ + nseen = 0; + TAILQ_FOREACH(span, &line->spans, line_entry) { + found = 0; + for (i = 0; i < nseen; i++) { + if (seen[i] == span->placement) { + found = 1; + break; + } + } + if (!found) { + seen = xreallocarray(seen, nseen + 1, + sizeof *seen); + seen[nseen++] = span->placement; + } + } + + for (i = 0; i < nseen; i++) { + placement = seen[i]; + 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(seen); +} + +/* Place an image at the cursor using the supplied input semantics. */ +static void +image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, + enum image_input input, u_int app_image_id, u_int app_placement_id, + int32_t z) +{ + struct screen *s = ctx->s; + struct grid *gd = s->grid; + struct image_placement *placement; + struct image_line *line; + u_int cx = s->cx, cy = s->cy; + u_int x, y, i, run, sx, sy, lines, origin_y = 0; + u_int hist_origin_y, region_height, remaining, chunk; + u_int cursor_x = 0, cursor_rows; + + image_get_size_in_cells(im, &sx, &sy); + if (sx > screen_size_x(s) - cx) + sx = screen_size_x(s) - cx; + if (sx == 0) + return; + + /* Kitty advances to the last image row; SIXEL advances past it. */ + cursor_rows = sy; + if (input == IMAGE_INPUT_KITTY) + cursor_rows--; + + /* Scroll images that move the cursor, or clip stationary placements. */ + if (image_has_no_cursor(im)) { + if (sy > screen_size_y(s) - cy) + sy = screen_size_y(s) - cy; + } else if (screen_size_y(s) - cy <= cursor_rows) { + lines = cursor_rows - (screen_size_y(s) - cy) + 1; + + /* screen_write_scrollup() clamps lines per call, so loop it. */ + region_height = s->rlower - s->rupper + 1; + if (region_height == 0) + region_height = 1; + remaining = lines; + while (remaining > 0) { + chunk = (remaining < region_height) ? remaining : + region_height; + screen_write_scrollup(ctx, chunk, bg); + remaining -= chunk; + } + + if (lines > cy) { + origin_y = lines - cy; + screen_write_cursormove(ctx, -1, 0, 0); + } else + screen_write_cursormove(ctx, -1, cy - lines, 0); + cy = s->cy; + sy -= origin_y; + cursor_rows -= origin_y; + } + + placement = image_placement_create(gd, im, input, app_image_id, + app_placement_id, z); + + /* Attach scrolled-off image rows to the available history lines. */ + hist_origin_y = origin_y; + if (hist_origin_y > gd->hsize) + hist_origin_y = gd->hsize; + for (y = 0; y < hist_origin_y; y++) { + line = image_line_get( + &gd->linedata[gd->hsize - hist_origin_y + y]); + image_extend_row(line, placement, cx, + origin_y - hist_origin_y + y, 0, sx); + } + + /* Attach visible runs, discarding SIXEL cells they completely cover. */ + for (y = 0; y < sy; y++) { + line = image_line_get(&gd->linedata[gd->hsize + cy + y]); + for (x = 0; x < sx; x += run) { + if (!image_cell_has_alpha(im, x, origin_y + y)) { + run = 1; + continue; + } + for (run = 1; x + run < sx; run++) { + if (!image_cell_has_alpha(im, x + run, + origin_y + y)) { + break; + } + } + if (input == IMAGE_INPUT_SIXEL) { + for (i = 0; i < run; i++) { + image_line_cover(line, placement, + cx + x + i, x + i, origin_y + y); + } + } + image_span_add(line, placement, cx + x, run, x, + origin_y + y); + } + } + + /* Schedule a redraw and apply the input protocol's cursor movement. */ + image_store_prune(gd->images); + image_redraw_area(ctx, cx, cy, sx, sy); + if (!image_has_no_cursor(im)) { + if (input == IMAGE_INPUT_KITTY) + cursor_x = cx + sx; + screen_write_cursormove(ctx, cursor_x, cy + cursor_rows, 0); + } +} + +/* Place an image received through SIXEL. */ +void +image_write_sixel(struct screen_write_ctx *ctx, struct image *im, u_int bg) +{ + image_write(ctx, im, bg, IMAGE_INPUT_SIXEL, 0, 0, 0); +} + +/* Place an image received through the Kitty graphics protocol. */ +void +image_write_kitty(struct screen_write_ctx *ctx, struct image *im, u_int bg, + u_int image_id, u_int placement_id, int32_t z) +{ + image_write(ctx, im, bg, IMAGE_INPUT_KITTY, image_id, placement_id, z); +} + +/* 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, IMAGE_INPUT_SIXEL, 0, 0, 0); + 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 every image span in a grid rectangle. */ +void +image_grid_remove(struct grid *gd, u_int x, u_int y, u_int sx, u_int sy) +{ + u_int row, rows = gd->hsize + gd->sy; + + 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_INPUT_ALL, NULL); + image_store_prune(gd->images); +} + +/* Redraw images under changed cells and remove those overwritten by input. */ +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_overwritten_spans(s->grid, x, + s->grid->hsize + y, sx, sy); +} diff --git a/image-kitty.c b/image-kitty.c index 90feb7ed4..b942bf49e 100644 --- a/image-kitty.c +++ b/image-kitty.c @@ -595,7 +595,7 @@ kitty_upload(struct tty *tty, struct image *im) } while (id == 0); entry->server_id = image_get_id(im); entry->kitty_id = id; - entry->order_id = im->order_id; + entry->order_id = image_get_order_id(im); entry->xpixel = tty->xpixel; entry->ypixel = tty->ypixel; entry->next_placement = 0; @@ -843,7 +843,7 @@ kitty_placements_free(struct kitty_source *source) for (placement = source->placements; placement != NULL; placement = next) { next = placement->next; - image_free(placement->server_id); + image_free(image_find(placement->server_id)); free(placement); } source->placements = NULL; @@ -854,7 +854,7 @@ static void kitty_source_free(struct kitty_source *source) { kitty_placements_free(source); - image_free(source->server_id); + image_free(image_find(source->server_id)); free(source); } @@ -908,11 +908,11 @@ kitty_source_set(struct kitty_context *kc, u_int id, struct image *im) old_id = source->server_id; old = image_find(old_id); if (old != NULL) - im->order_id = old->order_id; + image_set_order_id(im, image_get_order_id(old)); kitty_placements_free(source); - image_free(source->server_id); + image_free(image_find(source->server_id)); } - image_ref(image_get_id(im)); + image_ref(im); source->server_id = image_get_id(im); return (old_id); } @@ -945,10 +945,10 @@ kitty_placement_set(struct kitty_context *kc, const struct kitty_state *ks, old_id = placement->server_id; placement->z = ks->z; placement->virtual = ks->virtual; - image_ref(image_get_id(im)); + image_ref(im); placement->server_id = image_get_id(im); if (old_id != 0) - image_free(old_id); + image_free(image_find(old_id)); return (old_id); } @@ -964,7 +964,7 @@ kitty_source_get(struct kitty_context *kc, u_int id) return (NULL); im = image_find(source->server_id); if (im != NULL) - image_ref(image_get_id(im)); + image_ref(im); return (im); } @@ -981,7 +981,7 @@ kitty_prune_placements(struct kitty_source *source, struct grid *gd) if (image_grid_has_image(gd, placement->server_id)) goto keep; *pp = placement->next; - image_free(placement->server_id); + image_free(image_find(placement->server_id)); free(placement); removed++; continue; @@ -1040,7 +1040,7 @@ kitty_delete_images(void *state, struct screen_write_ctx *ctx, goto keep_placement; } *pp = placement->next; - image_free(placement->server_id); + image_free(image_find(placement->server_id)); free(placement); removed++; continue; @@ -1272,20 +1272,20 @@ kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, view.scaled_height = floor(view.height * height_scale * units); if (view.scaled_height < view.height) view.scaled_height = view.height; - view.x_offset = floor(x_offset * units); - view.y_offset = floor(y_offset * units); - if (view.x_offset > view.canvas_width || - view.scaled_width > view.canvas_width - view.x_offset || - view.y_offset > view.canvas_height || - view.scaled_height > view.canvas_height - view.y_offset) { + view.canvas_xoff = floor(x_offset * units); + view.canvas_yoff = floor(y_offset * units); + if (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); } /* Centre Unicode-placeholder images within their rectangle. */ if (ks->virtual && ks->columns != 0 && ks->rows != 0) { - view.x_offset += (view.canvas_width - view.x_offset - + view.canvas_xoff += (view.canvas_width - view.canvas_xoff - view.scaled_width) / 2; - view.y_offset += (view.canvas_height - view.y_offset - + view.canvas_yoff += (view.canvas_height - view.canvas_yoff - view.scaled_height) / 2; } /* Let the output terminal scale unpadded rectangular placements. */ @@ -1297,8 +1297,8 @@ kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, view.scaled_width = view.width; view.canvas_height = view.height; view.scaled_height = view.height; - view.x_offset = 0; - view.y_offset = 0; + view.canvas_xoff = 0; + view.canvas_yoff = 0; } im = image_create_view(source, &view); if (im != NULL && ks->no_cursor) @@ -1401,17 +1401,17 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, im = NULL; } else if (ks->virtual) { im = kitty_place_image(source, ks, xpixel, ypixel); - image_free(image_get_id(source)); + image_free(source); if (im != NULL) { (void)kitty_placement_set(kc, ks, im); - image_free(image_get_id(im)); + image_free(im); im = NULL; result->action = 'u'; result->status = KITTY_PARSE_OK; } } else { im = kitty_place_image(source, ks, xpixel, ypixel); - image_free(image_get_id(source)); + image_free(source); if (im != NULL) { result->replace_id = kitty_placement_set(kc, ks, im); @@ -1524,14 +1524,14 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, result->status = KITTY_PARSE_ERROR; else { (void)kitty_placement_set(kc, ks, im); - image_free(image_get_id(im)); + image_free(im); im = NULL; result->action = 'u'; } } else { im = NULL; } - image_free(image_get_id(source)); + image_free(source); } kitty_state_free(ks); return (im); diff --git a/image-sixel.c b/image-sixel.c index 68d266f80..606a5af03 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -1695,3 +1695,18 @@ sixel_redraw_start(struct tty *tty, u_int x, u_int y, u_int sx, u_int sy) tty_repeat_space(tty, sx); } } + +/* Write a SIXEL image and release the parsed data. */ +void +sixel_write(struct screen_write_ctx *ctx, struct sixel_image *si, u_int bg) +{ + struct image *im; + + im = sixel_to_image(si); + if (im == NULL) { + sixel_free(si); + return; + } + image_write_sixel(ctx, im, bg); + image_free(im); +} diff --git a/image.c b/image.c index 5759d08f9..b8d6eb68f 100644 --- a/image.c +++ b/image.c @@ -27,213 +27,50 @@ #include "tmux.h" -#define IMAGE_FLAG_NO_CURSOR 0x1 -#define IMAGE_FLAG_OWN_PIXELS 0x2 -#define IMAGE_Z_BELOW_BACKGROUND (INT32_MIN / 2) +/* An image. */ +struct image { + u_int id; + u_int references; + u_int flags; +#define IMAGE_FLAG_OWN_PIXELS 0x1 +#define IMAGE_FLAG_NO_CURSOR 0x2 -/* 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. */ + struct image *parent; + u_int source_id; + u_int order_id; + + /* Size in cells. */ + u_int sx; u_int sy; - u_int destination_x; /* Origin in terminal cells. */ - u_int destination_y; + + /* 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; + struct sixel_image *sixel; + struct image_cell *cells; + void *fallback_data; + + RB_ENTRY(image) entry; }; +RB_HEAD(images, image); +static struct images images = RB_INITIALIZER(&images); +static u_int image_next_id; -/* One contiguous row of a placement in the grid. */ -struct image_span { - u_int x; /* Owning grid column. */ - u_int sx; /* Number of image cells. */ - u_int source_x; /* Origin in image cells. */ - 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); - -/* Image spans attached to one grid line. */ -struct image_line { - struct image_spans spans; -}; - -enum image_input { - IMAGE_INPUT_ALL = -1, - IMAGE_INPUT_OVERWRITTEN = -2, - IMAGE_INPUT_SIXEL, - IMAGE_INPUT_KITTY -}; - -/* One logical image placement, shared by all of its row spans. */ -struct image_placement { - struct image_store *store; - struct image *image; - enum image_input input; - int placeholder; /* Uses Unicode placeholders. */ - u_int app_image_id; - u_int app_placement_id; - int32_t z; - uint64_t serial; /* Layer order. */ - struct image_spans spans; - TAILQ_ENTRY(image_placement) entry; -}; -TAILQ_HEAD(image_placements, image_placement); - -/* Placements belonging to one grid. */ -struct image_store { - uint64_t next_serial; - struct image_placements placements; -}; - -/* Spans saved while overlapping grid cells are moved. */ -struct image_move { - struct image_placement *placement; - u_int x; /* Destination grid column. */ - u_int sx; /* Number of image cells. */ - u_int source_x; /* Origin in image cells. */ - u_int source_y; -}; - -/* Pair source and destination placements during one grid copy. */ -struct image_placement_map { - struct image_placement *source; - struct image_placement *destination; -}; - -/* Placement mappings kept only for the duration of a grid copy. */ -struct image_copy_ctx { - struct grid *destination; - struct image_placement_map *maps; - size_t count; /* Placement mapping count. */ -}; - -static struct images images = RB_INITIALIZER(&images); -static u_int image_next_id; - -struct image_backend { - const char *name; - int flags; - void (*draw_rect)(struct tty *, const struct image_rect *); - void (*free)(struct tty *, int); -}; - -static const struct image_backend image_backend_fallback = { - "fallback", IMAGE_BACKEND_SCROLLS, NULL, NULL -}; -static const struct image_backend image_backend_kitty = { - "kitty", - IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED, - kitty_draw_rect, kitty_free_output_state -}; -static const struct image_backend image_backend_sixel = { - "sixel", IMAGE_BACKEND_GRAPHICAL, - sixel_draw_rect, sixel_free_output -}; - -/* Find the image backend supported by a terminal. */ -static const struct image_backend * -image_tty_find_backend(struct tty *tty) -{ - if (tty->term != NULL && tty->term->flags & TERM_KITTYGRAPHICS) - 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. */ -int -image_tty_update(struct tty *tty) -{ - const struct image_backend *backend; - - backend = image_tty_find_backend(tty); - if (tty->image_backend == backend) - return (0); - - if (tty->image_backend != NULL && tty->image_backend->free != NULL) - tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED)); - tty->image_data = NULL; - tty->image_backend = backend; - log_debug("%s: %s image backend is %s", __func__, - tty->client->name, backend->name); - return (1); -} - -/* Prepare the image backend to replace a redraw area. */ -void -image_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, - u_int height) -{ - image_tty_update(tty); - if (tty->image_backend == &image_backend_kitty) - 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. */ -void -image_redraw_finish(struct tty *tty) -{ - if (tty->image_backend == &image_backend_kitty) - kitty_redraw_finish(tty); -} - -/* Write out any image output the backend is still holding back. */ -void -image_draw_flush(struct tty *tty) -{ - image_tty_update(tty); - if (tty->image_backend == &image_backend_sixel) - sixel_flush_output(tty); -} - -/* Return the flags for a terminal's image backend. */ -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 -image_tty_geometry_changed(struct tty *tty) -{ - image_tty_update(tty); - if (tty->image_backend->free != NULL) - tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED)); -} - -/* Free image backend state for a terminal. */ -void -image_tty_free(struct tty *tty, int send) -{ - if (tty->image_backend != NULL && tty->image_backend->free != NULL) - tty->image_backend->free(tty, send); - tty->image_backend = NULL; - tty->image_data = NULL; -} - -/* Compare images by server ID. */ +/* Compare images by IDs. */ static int image_cmp(struct image *a, struct image *b) { @@ -245,514 +82,236 @@ image_cmp(struct image *a, struct image *b) } RB_GENERATE_STATIC(images, image, entry, image_cmp); -/* Return the ordering band for a placement. */ -static int -image_placement_band(const struct image_placement *placement) +/* Find an image by its ID. */ +struct image * +image_find(u_int id) { - if (placement->input == IMAGE_INPUT_SIXEL) - return (1); - if (placement->z < 0) - return (0); - return (2); + struct image find; + + find.id = id; + return (RB_FIND(images, &images, &find)); } -/* Compare two placements in logical drawing order. */ -static int -image_placement_cmp(const struct image_placement *a, - const struct image_placement *b) +/* Return the ID of an image. */ +u_int +image_get_id(struct image *im) { - int aband, bband; - - aband = image_placement_band(a); - bband = image_placement_band(b); - if (aband != bband) - return (aband < bband ? -1 : 1); - if (a->input == IMAGE_INPUT_KITTY && a->z != b->z) - return (a->z < b->z ? -1 : 1); - if (a->input == IMAGE_INPUT_KITTY && - a->image->order_id != b->image->order_id) { - return (a->image->order_id < b->image->order_id ? -1 : 1); - } - if (a->serial != b->serial) - return (a->serial < b->serial ? -1 : 1); - return (0); + return (im->id); } -/* Allocate the image store for a grid when first needed. */ -static struct image_store * -image_store_get(struct grid *gd) +/* 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_store *store = gd->images; + struct image *im; + u_int id; - if (store == NULL) { - store = xcalloc(1, sizeof *store); - TAILQ_INIT(&store->placements); - gd->images = store; - } - return (store); + do { + if (++image_next_id == 0) + image_next_id++; + id = image_next_id; + } while (image_find(id) != NULL); + + im = xcalloc(1, sizeof *im); + 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); } -/* Allocate the image span list for a grid line when first needed. */ -static struct image_line * -image_line_get(struct grid_line *gl) +/* 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_line *line = gl->images; + struct image *im; - if (line == NULL) { - line = xcalloc(1, sizeof *line); - TAILQ_INIT(&line->spans); - gl->images = line; - } - return (line); -} - -/* Create a logical image placement. */ -static struct image_placement * -image_placement_create(struct grid *gd, struct image *im, - enum image_input input, u_int app_image_id, u_int app_placement_id, - int32_t z) -{ - struct image_store *store = image_store_get(gd); - struct image_placement *placement; - - placement = xcalloc(1, sizeof *placement); - placement->store = store; - placement->image = im; - placement->input = input; - placement->app_image_id = app_image_id; - placement->app_placement_id = app_placement_id; - placement->z = z; - placement->serial = ++store->next_serial; - if (placement->serial == 0) - placement->serial = ++store->next_serial; - TAILQ_INIT(&placement->spans); - TAILQ_INSERT_TAIL(&store->placements, placement, entry); - image_ref(im->id); - return (placement); -} - -/* Free a placement which no longer has any spans. */ -static void -image_placement_free(struct image_placement *placement) -{ - if (!TAILQ_EMPTY(&placement->spans)) - fatalx("freeing image placement with spans"); - TAILQ_REMOVE(&placement->store->placements, placement, entry); - image_free(placement->image->id); - free(placement); -} - -/* Insert a span into both its line and placement lists. */ -static struct image_span * -image_span_add(struct image_line *line, struct image_placement *placement, - u_int x, u_int width, u_int source_x, u_int source_y) -{ - struct image_span *span, *at; - - if (width == 0) + if (w == 0 || h == 0 || cw < w || ch < h) return (NULL); - span = xcalloc(1, sizeof *span); - span->x = x; - span->sx = width; - span->source_x = source_x; - span->source_y = source_y; - span->line = line; - span->placement = placement; - TAILQ_FOREACH(at, &line->spans, line_entry) { - if (image_placement_cmp(placement, at->placement) < 0 || - (placement == at->placement && x < at->x)) { - TAILQ_INSERT_BEFORE(at, span, line_entry); - goto inserted; - } - } - TAILQ_INSERT_TAIL(&line->spans, span, line_entry); -inserted: - TAILQ_INSERT_TAIL(&placement->spans, span, placement_entry); - return (span); + 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 ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT / sizeof(struct image_cell)) + return (NULL); + + im = image_alloc(w, h, cw, ch, sx, sy, w * 4, pixels); + im->flags |= IMAGE_FLAG_OWN_PIXELS; + return (im); } -/* Unlink and free one span without pruning its placement. */ -static void -image_span_free(struct image_span *span) +/* 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) { - TAILQ_REMOVE(&span->line->spans, span, line_entry); - TAILQ_REMOVE(&span->placement->spans, span, placement_entry); - free(span); -} + u_char *pixels, *row, *src_row; + u_int src_x, src_y; + uint64_t x, y, off; -/* Remove a cell range from one placement or the selected input type. */ -static void -image_line_remove(struct image_line *line, u_int x, u_int width, - enum image_input input, - struct image_placement *only) -{ - struct image_span *span, *next; - u_int end, span_end, right; + 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; - if (line == NULL || width == 0) - return; - end = x + width; - if (end < x) - end = UINT_MAX; - TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) { - if (only != NULL && span->placement != only) - continue; - if (input == IMAGE_INPUT_OVERWRITTEN) { - /* Keep ordinary Kitty placements on text writes. */ - if (span->placement->input != IMAGE_INPUT_SIXEL && - !span->placement->placeholder) { - continue; + 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); } - } else if (input != IMAGE_INPUT_ALL) { - if (span->placement->input != input) - continue; } - span_end = span->x + span->sx; - 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); } + return (pixels); } -/* Remove placement records which have no remaining spans. */ -static void -image_store_prune(struct image_store *store) +/* 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_placement *placement, *next; + struct image *im; + u_char *pixels; + u_int w, h; - if (store == NULL) - return; - TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { - if (TAILQ_EMPTY(&placement->spans)) - image_placement_free(placement); + 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 || + (uint64_t)view->sx * view->sy > + IMAGE_SIZE_LIMIT / sizeof(struct image_cell) || + 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); + + 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; } + im->parent = src; + im->source_id = src->source_id; + im->order_id = src->order_id; + image_ref(src); + return (im); } -/* Remove SIXEL and placeholder spans overwritten by text. */ +/* Add a reference to an image. */ void -image_grid_remove_overwritten_spans(struct grid *gd, u_int x, u_int y, - u_int width, u_int height) +image_ref(struct image *im) { - u_int row; - - if (gd->images == NULL || width == 0 || height == 0) - return; - if (y >= gd->hsize + gd->sy) - return; - if (height > gd->hsize + gd->sy - y) - height = gd->hsize + gd->sy - y; - for (row = y; row < y + height; row++) { - image_line_remove(gd->linedata[row].images, x, width, - IMAGE_INPUT_OVERWRITTEN, NULL); - } - image_store_prune(gd->images); + if (im->references == UINT_MAX) + fatalx("invalid image reference %u", im->id); + im->references++; } -/* Free all image spans belonging to a grid line. */ +/* Free an image. */ void -image_grid_free_line(struct grid *gd, struct grid_line *gl) +image_free(struct image *im) { - 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); + 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); + if (im->sixel != NULL) + sixel_free(im->sixel); + fallback_free(im); + free(im->cells); + free(im); + } } -/* Free the empty image store when a grid is destroyed. */ +/* Return image size in pixels. */ void -image_grid_free(struct grid *gd) +image_get_size(struct image *im, u_int *w, u_int *h) { - if (gd->images == NULL) - return; - image_store_prune(gd->images); - if (!TAILQ_EMPTY(&gd->images->placements)) - fatalx("freeing grid with image placements"); - free(gd->images); - gd->images = NULL; + *w = im->width; + *h = im->height; } -/* Move image spans with a range of grid cells. */ +/* Return canvas size in pixels. */ void -image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, - u_int nx) +image_get_canvas_size(struct image *im, u_int *w, u_int *h) { - struct image_line *line; - struct image_span *span; - struct image_move *moves = NULL; - size_t count = 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; - - /* Save source spans before removing the overlapping cell ranges. */ - 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, count + 1, sizeof *moves); - moves[count].placement = span->placement; - moves[count].x = dx + start - px; - moves[count].sx = span_end - start; - moves[count].source_x = span->source_x + start - span->x; - moves[count].source_y = span->source_y; - count++; - } - - /* Replace both ranges and restore spans at their new columns. */ - image_line_remove(line, px, nx, IMAGE_INPUT_ALL, NULL); - image_line_remove(line, dx, nx, IMAGE_INPUT_ALL, NULL); - for (i = 0; i < count; 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); + *w = im->canvas_width; + *h = im->canvas_height; } -/* Find or create the destination placement for a copied source placement. */ -static struct image_placement * -image_copy_placement(struct image_copy_ctx *ctx, - struct image_placement *source) -{ - struct image_placement *placement; - size_t i; - - for (i = 0; i < ctx->count; i++) { - if (ctx->maps[i].source == source) - return (ctx->maps[i].destination); - } - placement = image_placement_create(ctx->destination, source->image, - source->input, source->app_image_id, source->app_placement_id, - source->z); - placement->placeholder = source->placeholder; - ctx->maps = xreallocarray(ctx->maps, ctx->count + 1, - sizeof *ctx->maps); - ctx->maps[ctx->count].source = source; - ctx->maps[ctx->count].destination = placement; - ctx->count++; - return (placement); -} - -/* Duplicate image spans alongside a group of grid lines. */ +/* Return image size in cells. */ void -image_grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, - u_int sy, u_int ny) +image_get_size_in_cells(struct image *im, u_int *sx, u_int *sy) { - struct image_copy_ctx ctx = { .destination = dst }; - struct image_placement *placement; - struct image_line *source_line, *destination_line; - struct image_span *span; - u_int row; - - for (row = 0; row < ny; row++) { - source_line = src->linedata[sy + row].images; - if (source_line == NULL) - continue; - destination_line = image_line_get(&dst->linedata[dy + row]); - TAILQ_FOREACH(span, &source_line->spans, line_entry) { - placement = image_copy_placement(&ctx, span->placement); - image_span_add(destination_line, placement, span->x, - span->sx, span->source_x, span->source_y); - } - } - free(ctx.maps); + *sx = im->sx; + *sy = im->sy; } -/* Copy clipped image spans between grid areas. */ +/* 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_grid_copy_area(struct grid *dst, u_int destination_x, - u_int destination_y, struct grid *src, u_int source_x, u_int source_y, - u_int sx, u_int sy) +image_size_in_cells(u_int w, u_int h, u_int xpixel, u_int ypixel, u_int *sx, + u_int *sy) { - struct image_copy_ctx ctx = { .destination = dst }; - struct image_placement *placement; - struct image_line *source_line, *destination_line; - struct image_span *span; - u_int row, start, end, span_end; - - if (dst == src || sx == 0 || sy == 0) - return; - if (destination_y >= dst->hsize + dst->sy || - source_y >= src->hsize + src->sy) { - return; - } - if (sy > dst->hsize + dst->sy - destination_y) - sy = dst->hsize + dst->sy - destination_y; - if (sy > src->hsize + src->sy - source_y) - sy = src->hsize + src->sy - source_y; - end = source_x + sx; - if (end < source_x) - end = UINT_MAX; - - /* Copy clipped spans, sharing one destination placement per source. */ - for (row = 0; row < sy; row++) { - source_line = src->linedata[source_y + row].images; - if (source_line == NULL) - continue; - destination_line = image_line_get( - &dst->linedata[destination_y + row]); - TAILQ_FOREACH(span, &source_line->spans, line_entry) { - span_end = span->x + span->sx; - start = (span->x > source_x ? span->x : source_x); - if (start >= end || span_end <= source_x) - continue; - if (span_end > end) - span_end = end; - - placement = image_copy_placement(&ctx, span->placement); - image_span_add(destination_line, placement, - destination_x + start - source_x, span_end - start, - span->source_x + start - span->x, span->source_y); - } - } - free(ctx.maps); -} - -/* Return whether a grid line contains any image spans. */ -int -image_grid_line_has_images(const struct grid_line *gl) -{ - return (gl->images != NULL && !TAILQ_EMPTY(&gl->images->spans)); -} - -/* Return whether a grid rectangle contains any image spans. */ -int -image_grid_area_has_images(struct grid *gd, u_int x, u_int y, u_int width, - u_int height) -{ - struct image_line *line; - struct image_span *span; - u_int row, end; - - if (gd->images == NULL || width == 0 || height == 0 || - y >= gd->hsize + gd->sy) { - return (0); - } - end = x + width; - if (height > gd->hsize + gd->sy - y) - height = gd->hsize + gd->sy - y; - for (row = y; row < y + height; 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); -} - -/* Find the resolved placeholder at one grid cell. */ -int -image_grid_get_placeholder(struct grid *gd, u_int x, u_int y, - struct kitty_placeholder *placeholder) -{ - struct image_line *line; - struct image_span *span, *found = NULL; - - if (y >= gd->hsize + gd->sy || - (line = gd->linedata[y].images) == NULL) { - return (0); - } - TAILQ_FOREACH(span, &line->spans, line_entry) { - if (!span->placement->placeholder) - continue; - if (x < span->x) - continue; - if (x < span->x + span->sx) - found = span; - } - if (found == NULL) - return (0); - placeholder->image = found->placement->image; - placeholder->source_x = found->source_x + x - found->x; - placeholder->source_y = found->source_y; - placeholder->image_id = found->placement->app_image_id; - placeholder->placement_id = found->placement->app_placement_id; - placeholder->z = found->placement->z; - return (1); -} - -/* Add one Kitty Unicode-placeholder cell to an image placement. */ -void -image_place_cell_kitty(struct screen_write_ctx *ctx, struct image *im, - u_int x, u_int y, u_int source_x, u_int source_y, u_int image_id, - u_int placement_id, int32_t z) -{ - struct grid *gd = ctx->s->grid; - struct image_store *store = image_store_get(gd); - struct image_placement *placement = NULL, *candidate; - struct image_line *line; - struct image_span *span; - - /* Reuse a matching placeholder placement in this grid. */ - TAILQ_FOREACH_REVERSE(candidate, &store->placements, - image_placements, entry) { - if (!candidate->placeholder) - continue; - if (candidate->input == IMAGE_INPUT_KITTY && - candidate->image == im && - candidate->app_image_id == image_id && - candidate->app_placement_id == placement_id && - candidate->z == z) { - placement = candidate; - break; - } - } - if (placement == NULL) { - placement = image_placement_create(gd, im, IMAGE_INPUT_KITTY, - image_id, placement_id, z); - placement->placeholder = 1; - } - /* Extend an adjacent source run or add a new span for this cell. */ - line = image_line_get(&gd->linedata[gd->hsize + y]); - TAILQ_FOREACH(span, &line->spans, line_entry) { - if (span->placement == placement && span->x + span->sx == x && - span->source_y == source_y && - span->source_x + span->sx == source_x) { - span->sx++; - image_redraw_area(ctx, x, y, 1, 1); - return; - } - } - image_span_add(line, placement, x, 1, source_x, source_y); - image_redraw_area(ctx, x, y, 1, 1); + if (xpixel == 0) + xpixel = 8; + if (ypixel == 0) + ypixel = 16; + *sx = w / xpixel + (w % xpixel != 0); + *sy = h / ypixel + (h % ypixel != 0); } /* Average a rectangle of image pixels into one fallback sample. */ @@ -837,66 +396,6 @@ image_make_cells(struct image *im) } } -/* Find an image by server ID. */ -struct image * -image_find(u_int id) -{ - struct image find, *im; - - find.id = id; - im = RB_FIND(images, &images, &find); - return (im); -} - -/* Return an image's server ID. */ -u_int -image_get_id(const struct image *im) -{ - return (im->id); -} - -/* Return an image's pixel dimensions. */ -void -image_get_size(const struct image *im, u_int *width, u_int *height) -{ - if (width != NULL) - *width = im->width; - if (height != NULL) - *height = im->height; -} - -/* Return an image canvas's pixel dimensions. */ -void -image_get_canvas_size(const struct image *im, u_int *width, - u_int *height) -{ - if (width != NULL) - *width = im->canvas_width; - if (height != NULL) - *height = im->canvas_height; -} - -/* Return an image's cell dimensions. */ -void -image_get_size_in_cells(const struct image *im, u_int *sx, u_int *sy) -{ - if (sx != NULL) - *sx = im->sx; - if (sy != NULL) - *sy = im->sy; -} - -/* Return an image's RGBA pixels and layout. */ -const u_char * -image_get_pixels(const struct image *im, size_t *stride, size_t *size) -{ - if (stride != NULL) - *stride = im->stride; - if (size != NULL) - *size = im->size; - return (im->pixels); -} - /* Suppress cursor movement when writing an image. */ void image_set_no_cursor(struct image *im) @@ -906,7 +405,7 @@ image_set_no_cursor(struct image *im) /* Return an image's original SIXEL data. */ struct sixel_image * -image_get_sixel(const struct image *im) +image_get_sixel(struct image *im) { return (im->sixel); } @@ -918,225 +417,46 @@ image_set_sixel(struct image *im, struct sixel_image *si) im->sixel = si; } -/* Return the image for a drawing rectangle. */ -struct image * -image_rect_get_image(const struct image_rect *rectangle) +/* Return the original source image ID. */ +u_int +image_get_source_id(struct image *im) { - return (rectangle->image); + return (im->source_id); } -/* Return the source and destination coordinates of a drawing rectangle. */ +/* Return the image's original ordering ID. */ +u_int +image_get_order_id(struct image *im) +{ + return (im->order_id); +} + +/* Set the original ordering ID after replacing an image. */ void -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 *destination_x, u_int *destination_y) +image_set_order_id(struct image *im, u_int id) { - *source_x = rectangle->source_x; - *source_y = rectangle->source_y; - *width = rectangle->sx; - *height = rectangle->sy; - *destination_x = rectangle->destination_x; - *destination_y = rectangle->destination_y; + im->order_id = id; } -/* Return the output z-index for a drawing rectangle. */ -int64_t -image_rect_get_z(const struct image_rect *rectangle) +/* Return whether writing the image should leave the cursor in place. */ +int +image_has_no_cursor(struct image *im) { - return (rectangle->z); + return (!!(im->flags & IMAGE_FLAG_NO_CURSOR)); } -/* Create and register an immutable image. */ -static struct image * -image_alloc(u_int width, u_int height, u_int canvas_width, - u_int canvas_height, u_int sx, u_int sy, size_t stride, u_char *pixels) +/* Return cached fallback output data. */ +void * +image_get_fallback_data(struct image *im) { - struct image *im; - - im = xcalloc(1, sizeof *im); - do { - if (++image_next_id == 0) - image_next_id++; - im->id = image_next_id; - } while (image_find(im->id) != NULL); - - im->references = 1; - im->source_id = im->id; - im->order_id = im->id; - im->width = width; - im->height = height; - im->canvas_width = canvas_width; - im->canvas_height = canvas_height; - im->sx = sx; - im->sy = sy; - im->stride = stride; - im->size = (size_t)width * height * 4; - im->pixels = pixels; - - RB_INSERT(images, &images, im); - log_debug("%s: image %u is %ux%u pixels on %ux%u canvas, " - "%ux%u cells", __func__, im->id, width, height, canvas_width, - canvas_height, sx, sy); - return (im); + return (im->fallback_data); } -/* Create an image from decoded pixel data. */ -struct image * -image_create(u_int width, u_int height, u_int canvas_width, - u_int canvas_height, u_int sx, u_int sy, u_char *pixels) -{ - struct image *im; - - if (width == 0 || height == 0 || canvas_width < width || - canvas_height < height || sx == 0 || sy == 0 || pixels == NULL) { - return (NULL); - } - if ((uint64_t)width * height > SIZE_MAX / 4) - return (NULL); - if ((uint64_t)sx * sy > SIZE_MAX / sizeof *im->cells || - sx > USHRT_MAX || sy > USHRT_MAX) { - return (NULL); - } - if ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT / sizeof *im->cells) - return (NULL); - im = image_alloc(width, height, canvas_width, canvas_height, sx, sy, - (size_t)width * 4, pixels); - im->flags |= IMAGE_FLAG_OWN_PIXELS; - return (im); -} - -/* Create a cropped and scaled image view with transparent padding. */ -struct image * -image_create_view(struct image *source, const struct image_view *view) -{ - struct image *im; - u_char *pixels, *row; - const u_char *source_row; - u_int padded_width, padded_height, x, y, source_x, source_y; - int share_pixels = 1; - - /* Validate the source crop and placement dimensions. */ - if (source == NULL) - return (NULL); - if (view->x >= source->width || view->y >= source->height) - return (NULL); - if (view->width == 0 || view->height == 0 || view->scaled_width == 0 || - view->scaled_height == 0 || view->sx == 0 || view->sy == 0) { - return (NULL); - } - if (view->width > source->width - view->x || - view->height > source->height - view->y) { - return (NULL); - } - if (view->sx > USHRT_MAX || view->sy > USHRT_MAX) - return (NULL); - if ((uint64_t)view->sx * view->sy > SIZE_MAX / sizeof *im->cells) - return (NULL); - if ((uint64_t)view->sx * view->sy > - IMAGE_SIZE_LIMIT / sizeof *im->cells) { - return (NULL); - } - - /* Keep the padded raster inside the logical canvas. */ - if (view->x_offset > UINT_MAX - view->scaled_width || - view->y_offset > UINT_MAX - view->scaled_height) { - return (NULL); - } - padded_width = view->scaled_width + view->x_offset; - padded_height = view->scaled_height + view->y_offset; - if (padded_width > view->canvas_width || - padded_height > view->canvas_height) { - return (NULL); - } - - /* Share pixels when no scaling or padding is needed. */ - if (view->scaled_width != view->width || - view->scaled_height != view->height || - view->x_offset != 0 || view->y_offset != 0) { - share_pixels = 0; - } - if (share_pixels) { - im = image_alloc(view->width, view->height, view->canvas_width, - view->canvas_height, view->sx, view->sy, source->stride, - source->pixels + (size_t)view->y * source->stride + - (size_t)view->x * 4); - } else { - /* Resample the crop with transparent padding. */ - if ((uint64_t)padded_width * padded_height > - IMAGE_SIZE_LIMIT / 4) { - return (NULL); - } - pixels = xcalloc((size_t)padded_width * padded_height, 4); - for (y = 0; y < view->scaled_height; y++) { - source_y = view->y + (uint64_t)y * view->height / - view->scaled_height; - source_row = source->pixels + - (size_t)source_y * source->stride; - source_row += (size_t)view->x * 4; - row = pixels + - ((size_t)(y + view->y_offset) * padded_width + - view->x_offset) * 4; - if (view->scaled_width == view->width) { - memcpy(row, source_row, - (size_t)view->width * 4); - continue; - } - for (x = 0; x < view->scaled_width; x++) { - source_x = (uint64_t)x * view->width / - view->scaled_width; - memcpy(row + (size_t)x * 4, - source_row + (size_t)source_x * 4, 4); - } - } - im = image_alloc(padded_width, padded_height, - view->canvas_width, view->canvas_height, view->sx, view->sy, - (size_t)padded_width * 4, pixels); - im->flags |= IMAGE_FLAG_OWN_PIXELS; - } - - /* Retain the source for both shared pixels and placement identity. */ - im->parent_id = source->id; - im->source_id = source->source_id; - im->order_id = source->order_id; - image_ref(source->id); - return (im); -} - -/* Add a reference to an image. */ +/* Store cached fallback output data. */ void -image_ref(u_int id) +image_set_fallback_data(struct image *im, void *data) { - struct image *im = image_find(id); - - if (im == NULL) - fatalx("reference to missing image %u", id); - if (im->references == UINT_MAX) - fatalx("too many references to image %u", id); - im->references++; -} - -/* Drop a reference to an image. */ -void -image_free(u_int id) -{ - struct image *im = image_find(id); - - if (im == NULL) - fatalx("free of missing image %u", id); - if (--im->references != 0) - return; - - log_debug("%s: freeing image %u", __func__, id); - RB_REMOVE(images, &images, im); - if (im->flags & IMAGE_FLAG_OWN_PIXELS) - free(im->pixels); - if (im->parent_id != 0) - image_free(im->parent_id); - if (im->sixel != NULL) - sixel_free(im->sixel); - fallback_free(im); - free(im->cells); - free(im); + im->fallback_data = data; } /* Return a precomputed fallback cell sample. */ @@ -1150,49 +470,9 @@ image_get_cell(struct image *im, u_int x, u_int y) return (&im->cells[(size_t)y * im->sx + x]); } -/* Return one for a fallback cell, minus one to continue along an image line. */ -int -image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, - const struct grid_cell *gc, struct grid_cell *out) -{ - struct image_line *line; - struct image_span *span, *found = NULL; - struct image_placement *placement; - - if (image_backend_flags(tty) & IMAGE_BACKEND_GRAPHICAL || - y >= s->grid->sy) { - return (0); - } - line = s->grid->linedata[s->grid->hsize + y].images; - if (line == NULL) - return (0); - TAILQ_FOREACH(span, &line->spans, line_entry) { - if (x >= span->x && x < span->x + span->sx) - found = span; - } - if (found == NULL) - return (-1); - placement = found->placement; - if (placement->input == IMAGE_INPUT_KITTY && placement->z < 0) { - if (!kitty_cell_is_placeholder(gc)) { - if (gc->data.size != 1) - return (-1); - if (gc->data.data[0] != ' ') - return (-1); - } - if (placement->z < IMAGE_Z_BELOW_BACKGROUND && - !COLOUR_DEFAULT(gc->bg)) { - return (-1); - } - } - fallback_get_cell(tty, placement->image, - found->source_x + x - found->x, found->source_y, gc, out); - return (1); -} - /* Convert an image cell rectangle to pixel coordinates. */ void -image_get_pixel_rect(const struct image *im, u_int x, u_int y, +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) { @@ -1232,19 +512,6 @@ image_get_pixel_rect(const struct image *im, u_int x, u_int y, *pheight = bottom - top; } -/* Calculate the cell dimensions required for pixel dimensions. */ -void -image_size_in_cells(u_int width, u_int height, u_int xpixel, u_int ypixel, - u_int *sx, u_int *sy) -{ - if (xpixel == 0) - xpixel = 8; - if (ypixel == 0) - ypixel = 16; - *sx = width / xpixel + (width % xpixel != 0); - *sy = height / ypixel + (height % ypixel != 0); -} - /* Decode a base64 payload with a size limit. */ u_char * image_base64_decode(const char *data, size_t len, size_t limit, size_t *size) @@ -1313,615 +580,3 @@ image_png_decode(const u_char *data, size_t size, size_t limit, u_int *width, png_image_free(&pi); return (pixels); } - -/* Remove one placement and all of its spans. */ -static void -image_remove_placement(struct image_placement *placement) -{ - struct image_span *span, *next; - - TAILQ_FOREACH_SAFE(span, &placement->spans, placement_entry, next) - image_span_free(span); - image_placement_free(placement); -} - -/* Clear image placements with an internal image ID from a screen. */ -void -image_clear(struct screen_write_ctx *ctx, u_int id) -{ - struct image_store *store = ctx->s->grid->images; - struct image_placement *placement, *next; - - if (store == NULL) - return; - TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { - if (id != 0) { - if (placement->image->source_id != id) { - if (placement->placeholder) - continue; - if (placement->image->id != id) - continue; - } - } - image_remove_placement(placement); - } - if (ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -} - -/* Return whether a grid still references an image or its source. */ -int -image_grid_has_image(struct grid *gd, u_int id) -{ - struct image_placement *placement; - - if (gd->images == NULL) - return (0); - TAILQ_FOREACH(placement, &gd->images->placements, entry) { - if (placement->image->id == id) - return (1); - if (placement->image->source_id == id) - return (1); - } - return (0); -} - -/* Return whether a placement intersects the selected screen cells. */ -static int -image_placement_intersects(struct grid *gd, struct image_placement *placement, - const struct kitty_parse_result *result) -{ - struct image_line *line; - struct image_span *span; - u_int x = result->x, y = result->y, row; - char how = result->delete; - int match_x = 0, match_y = 0; - - if (how >= 'A') { - if (how <= 'Z') - how += 'a' - 'A'; - } - switch (how) { - case 'p': case 'q': case 'c': - match_y = 1; - /* FALLTHROUGH */ - case 'x': - match_x = 1; - break; - case 'y': - match_y = 1; - break; - } - for (row = 0; row < gd->sy; row++) { - if (match_y) { - if (y == 0) - continue; - if (row != y - 1) - continue; - } - line = gd->linedata[gd->hsize + row].images; - if (line == NULL) - continue; - TAILQ_FOREACH(span, &line->spans, line_entry) { - if (span->placement != placement) - continue; - if (span->x >= gd->sx) - continue; - if (match_x) { - if (x == 0) - continue; - if (x > gd->sx) - continue; - if (x - 1 < span->x) - continue; - if (x - 1 >= span->x + span->sx) - continue; - } - return (1); - } - } - return (0); -} - -/* Clear ordinary Kitty placements selected by a graphics delete command. */ -void -image_clear_kitty(struct screen_write_ctx *ctx, - const struct kitty_parse_result *result) -{ - struct grid *gd = ctx->s->grid; - struct image_store *store = gd->images; - struct image_placement *placement, *next; - struct kitty_parse_result selection = *result; - char how = result->delete; - int matched; - u_int placement_id = result->placement_id; - - if (store == NULL) - return; - if (how >= 'A') { - if (how <= 'Z') - how += 'a' - 'A'; - } - if (how == 'c') { - selection.x = ctx->s->cx + 1; - selection.y = ctx->s->cy + 1; - } - TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { - if (placement->input != IMAGE_INPUT_KITTY) - continue; - if (placement->placeholder) - continue; - matched = 0; - switch (how) { - case 'i': - if (placement->app_image_id != result->image_id) - break; - if (placement_id != 0) { - if (placement->app_placement_id != placement_id) - break; - } - matched = 1; - break; - case 'r': - if (placement->app_image_id < result->x) - break; - if (placement->app_image_id > result->y) - break; - matched = 1; - break; - case 'z': - if (placement->z == result->z) - matched = 1; - break; - case 'q': - if (placement->z != result->z) - break; - /* FALLTHROUGH */ - case 'a': case 'c': case 'p': case 'x': case 'y': - matched = image_placement_intersects(gd, placement, - &selection); - break; - } - if (matched) - image_remove_placement(placement); - } - if (ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; -} - -/* Record window damage for image layers in a screen area. */ -void -image_redraw_area(struct screen_write_ctx *ctx, u_int px, u_int py, u_int nx, - u_int ny) -{ - struct window_pane *wp = ctx->wp; - - if (wp == NULL) - return; - if (!image_grid_area_has_images(ctx->s->grid, px, - ctx->s->grid->hsize + py, nx, ny)) { - return; - } - redraw_damage_window(wp->window, wp->xoff + px, wp->yoff + py, nx, - ny); -} - -/* Redraw all image layers on a screen. */ -void -image_redraw_all(struct screen_write_ctx *ctx) -{ - image_redraw_area(ctx, 0, 0, screen_size_x(ctx->s), - screen_size_y(ctx->s)); -} - -/* Record image damage within the current scroll region. */ -void -image_redraw_scroll(struct screen_write_ctx *ctx) -{ - struct screen *s = ctx->s; - struct window_pane *wp = ctx->wp; - - if (wp == NULL) - return; - if (!image_grid_area_has_images(s->grid, 0, s->grid->hsize + s->rupper, - screen_size_x(s), s->rlower - s->rupper + 1)) { - return; - } - redraw_damage_window_scroll(wp->window, wp->xoff, - wp->yoff + s->rupper, screen_size_x(s), - s->rlower - s->rupper + 1); -} - -/* Draw a clipped part of one image span. */ -static void -image_draw_span(const struct image_backend *backend, struct tty *tty, - struct image_span *span, u_int start, u_int end, - u_int px, u_int atx, u_int aty) -{ - struct image_placement *placement = span->placement; - struct image_rect rectangle; - - rectangle.image = placement->image; - if (placement->input == IMAGE_INPUT_SIXEL) - rectangle.z = 0; - else if (placement->z >= 0) - rectangle.z = (int64_t)placement->z + 1; - else - rectangle.z = placement->z; - rectangle.source_x = span->source_x + start - span->x; - rectangle.source_y = span->source_y; - rectangle.sx = end - start; - rectangle.sy = 1; - rectangle.destination_x = atx + start - px; - rectangle.destination_y = aty; - backend->draw_rect(tty, &rectangle); -} - -/* Return whether a cell contains a glyph or text decoration. */ -static int -image_cell_has_text(struct grid *gd, u_int x, u_int y) -{ - struct grid_cell gc; - - grid_view_get_cell(gd, x, y, &gc); - if (kitty_cell_is_placeholder(&gc)) - return (0); - if (gc.data.size != 1 || gc.data.data[0] != ' ') - return (1); - return (gc.attr != 0); -} - -/* Draw a span's graphical image layers before or after its text. */ -void -image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, - u_int nx, u_int atx, u_int aty, int before) -{ - const struct image_backend *backend; - struct image_line *line; - struct image_span *span; - struct image_placement *placement; - u_int start, end, span_end, draw_end; - int blank_only; - - image_tty_update(tty); - backend = tty->image_backend; - if (~backend->flags & IMAGE_BACKEND_GRAPHICAL) - return; - if (py >= s->grid->sy) - return; - line = s->grid->linedata[s->grid->hsize + py].images; - if (line == NULL) - return; - end = px + nx; - TAILQ_FOREACH(span, &line->spans, line_entry) { - placement = span->placement; - blank_only = 0; - - /* Emulate Kitty's negative layers with SIXEL. */ - if (placement->input == IMAGE_INPUT_KITTY && - placement->z < 0) { - if (backend == &image_backend_sixel && - placement->z >= IMAGE_Z_BELOW_BACKGROUND) { - if (before) - continue; - blank_only = 1; - } else if (!before) - continue; - } else if (before) - continue; - 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; - if (!blank_only) { - image_draw_span(backend, tty, span, start, span_end, - px, atx, aty); - continue; - } - /* Draw below-text SIXEL layers only on cells without text. */ - while (start < span_end) { - while (start < span_end && - image_cell_has_text(s->grid, start, py)) - start++; - draw_end = start; - while (draw_end < span_end && - !image_cell_has_text(s->grid, draw_end, py)) - draw_end++; - if (start < draw_end) { - image_draw_span(backend, tty, span, start, - draw_end, px, atx, aty); - } - start = draw_end; - } - } -} - -/* Return whether an image cell contains at least one nontransparent pixel. */ -static int -image_cell_has_alpha(struct image *im, u_int x, u_int y) -{ - u_int px, py, sx, sy, xx, yy; - const u_char *pixels; - - image_get_pixel_rect(im, x, y, 1, 1, &px, &py, &sx, &sy); - if (sx == 0 || sy == 0) - return (0); - pixels = im->pixels; - for (yy = py; yy < py + sy; yy++) { - for (xx = px; xx < px + sx; xx++) { - if (pixels[(size_t)yy * im->stride + - (size_t)xx * 4 + 3] != 0) { - return (1); - } - } - } - return (0); -} - -/* Return whether a new cell's pixels completely cover the old cell. */ -static int -image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old, - u_int ox, u_int oy) -{ - u_int npx, npy, nsx, nsy, opx, opy, osx, osy, xx, yy; - const u_char *np, *op; - - if (new->canvas_width / new->sx != old->canvas_width / old->sx || - new->canvas_height / new->sy != old->canvas_height / old->sy) { - return (0); - } - image_get_pixel_rect(new, nx, ny, 1, 1, &npx, &npy, &nsx, &nsy); - image_get_pixel_rect(old, ox, oy, 1, 1, &opx, &opy, &osx, &osy); - - for (yy = 0; yy < osy; yy++) { - op = old->pixels + (size_t)(opy + yy) * old->stride + - (size_t)opx * 4; - np = new->pixels + (size_t)(npy + yy) * new->stride + - (size_t)npx * 4; - for (xx = 0; xx < osx; xx++) { - if (op[xx * 4 + 3] == 0) - continue; - if (yy >= nsy || xx >= nsx || np[xx * 4 + 3] != 255) - return (0); - } - } - return (1); -} - -/* Discard cells of older SIXEL placements fully covered by a new image. */ -static void -image_line_cover(struct image_line *line, struct image_placement *placement, - u_int x, u_int source_x, u_int source_y) -{ - struct image_span *span, *next; - struct image_placement *old; - - TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) { - old = span->placement; - if (old == placement || old->input != IMAGE_INPUT_SIXEL) - continue; - if (x < span->x || x >= span->x + span->sx) - continue; - if (!image_cell_covers(placement->image, source_x, source_y, - old->image, span->source_x + x - span->x, span->source_y)) { - continue; - } - image_line_remove(line, x, 1, IMAGE_INPUT_ALL, old); - } -} - -/* Add spans for one row of a placement between two source columns. */ -static void -image_extend_row(struct image_line *line, struct image_placement *placement, - u_int cx, u_int source_y, u_int old_end, u_int new_end) -{ - struct image *im = placement->image; - u_int x, run; - - for (x = old_end; x < new_end; x += run) { - if (!image_cell_has_alpha(im, x, source_y)) { - run = 1; - continue; - } - for (run = 1; x + run < new_end; run++) { - if (!image_cell_has_alpha(im, x + run, source_y)) - break; - } - image_span_add(line, placement, cx + x, run, x, source_y); - } -} - -/* Extend image placements to reveal clipped columns when a pane grows. */ -void -image_grid_resize_width(struct grid *gd, u_int new_sx) -{ - struct grid_line *gl; - struct image_line *line; - struct image_span *span; - struct image_placement *placement; - struct image_placement **seen = NULL; - size_t nseen, i; - u_int row, cx, end_x, avail; - u_int source_y; - int found; - - if (gd->images == NULL) - return; - for (row = 0; row < gd->hsize + gd->sy; row++) { - gl = &gd->linedata[row]; - line = gl->images; - if (line == NULL) - continue; - - /* Collect placements before modifying their spans. */ - nseen = 0; - TAILQ_FOREACH(span, &line->spans, line_entry) { - found = 0; - for (i = 0; i < nseen; i++) { - if (seen[i] == span->placement) { - found = 1; - break; - } - } - if (!found) { - seen = xreallocarray(seen, nseen + 1, - sizeof *seen); - seen[nseen++] = span->placement; - } - } - - for (i = 0; i < nseen; i++) { - placement = seen[i]; - 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; - if (avail > placement->image->sx) - avail = placement->image->sx; - if (avail <= end_x) - continue; - image_extend_row(line, placement, cx, source_y, end_x, - avail); - } - } - free(seen); -} - -/* Place an image at the cursor using the supplied input semantics. */ -static void -image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, - enum image_input input, u_int app_image_id, u_int app_placement_id, - int32_t z) -{ - struct screen *s = ctx->s; - struct grid *gd = s->grid; - struct image_placement *placement; - struct image_line *line; - u_int cx = s->cx, cy = s->cy; - u_int x, y, i, run, sx, sy, lines, origin_y = 0; - u_int hist_origin_y, region_height, remaining, chunk; - u_int cursor_x = 0, cursor_rows; - - sx = im->sx; - if (sx > screen_size_x(s) - cx) - sx = screen_size_x(s) - cx; - sy = im->sy; - if (sx == 0) - return; - - /* Kitty advances to the last image row; SIXEL advances past it. */ - cursor_rows = sy; - if (input == IMAGE_INPUT_KITTY) - cursor_rows--; - - /* Scroll images that move the cursor, or clip stationary placements. */ - if (im->flags & IMAGE_FLAG_NO_CURSOR) { - if (sy > screen_size_y(s) - cy) - sy = screen_size_y(s) - cy; - } else if (screen_size_y(s) - cy <= cursor_rows) { - lines = cursor_rows - (screen_size_y(s) - cy) + 1; - - /* screen_write_scrollup() clamps lines per call, so loop it. */ - region_height = s->rlower - s->rupper + 1; - if (region_height == 0) - region_height = 1; - remaining = lines; - while (remaining > 0) { - chunk = (remaining < region_height) ? remaining : - region_height; - screen_write_scrollup(ctx, chunk, bg); - remaining -= chunk; - } - - if (lines > cy) { - origin_y = lines - cy; - screen_write_cursormove(ctx, -1, 0, 0); - } else - screen_write_cursormove(ctx, -1, cy - lines, 0); - cy = s->cy; - sy -= origin_y; - cursor_rows -= origin_y; - } - - placement = image_placement_create(gd, im, input, app_image_id, - app_placement_id, z); - - /* Attach scrolled-off image rows to the available history lines. */ - hist_origin_y = origin_y; - if (hist_origin_y > gd->hsize) - hist_origin_y = gd->hsize; - for (y = 0; y < hist_origin_y; y++) { - line = image_line_get( - &gd->linedata[gd->hsize - hist_origin_y + y]); - image_extend_row(line, placement, cx, - origin_y - hist_origin_y + y, 0, sx); - } - - /* Attach visible runs, discarding SIXEL cells they completely cover. */ - for (y = 0; y < sy; y++) { - line = image_line_get(&gd->linedata[gd->hsize + cy + y]); - for (x = 0; x < sx; x += run) { - if (!image_cell_has_alpha(im, x, origin_y + y)) { - run = 1; - continue; - } - for (run = 1; x + run < sx; run++) { - if (!image_cell_has_alpha(im, x + run, - origin_y + y)) { - break; - } - } - if (input == IMAGE_INPUT_SIXEL) { - for (i = 0; i < run; i++) { - image_line_cover(line, placement, - cx + x + i, x + i, origin_y + y); - } - } - image_span_add(line, placement, cx + x, run, x, - origin_y + y); - } - } - - /* Schedule a redraw and apply the input protocol's cursor movement. */ - image_store_prune(gd->images); - image_redraw_area(ctx, cx, cy, sx, sy); - if (!(im->flags & IMAGE_FLAG_NO_CURSOR)) { - if (input == IMAGE_INPUT_KITTY) - cursor_x = cx + sx; - screen_write_cursormove(ctx, cursor_x, cy + cursor_rows, 0); - } -} - -/* Place an image received through SIXEL. */ -void -image_write_sixel(struct screen_write_ctx *ctx, struct image *im, u_int bg) -{ - image_write(ctx, im, bg, IMAGE_INPUT_SIXEL, 0, 0, 0); -} - -/* Place an image received through the Kitty graphics protocol. */ -void -image_write_kitty(struct screen_write_ctx *ctx, struct image *im, u_int bg, - u_int image_id, u_int placement_id, int32_t z) -{ - image_write(ctx, im, bg, IMAGE_INPUT_KITTY, image_id, placement_id, z); -} diff --git a/input.c b/input.c index 7b9c77681..2be86ec03 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.276 2026/10/13 10:21:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2032,6 +2032,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; @@ -2093,13 +2094,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); @@ -2666,7 +2667,7 @@ input_dcs_dispatch(struct input_ctx *ictx) p2 = 0; si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel); if (si != NULL) - screen_write_sixelimage(sctx, si, ictx->cell.cell.bg); + sixel_write(sctx, si, ictx->cell.cell.bg); } #endif @@ -2867,7 +2868,7 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len) } else if (im != NULL) { image_write_kitty(sctx, im, ictx->cell.cell.bg, result.image_id, result.placement_id, result.z); - image_free(image_get_id(im)); + image_free(im); } if (result.action != 'd') input_reply_kitty(ictx, &result, "OK"); @@ -3336,6 +3337,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) @@ -3355,6 +3402,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) @@ -3426,6 +3475,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/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/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); 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 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) 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-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 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 diff --git a/screen-write.c b/screen-write.c index 39ab397eb..866401e39 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 @@ -34,10 +34,6 @@ static void screen_write_collect_clear(struct screen_write_ctx *, u_int, static void screen_write_collect_scroll(struct screen_write_ctx *, u_int); static void screen_write_collect_flush(struct screen_write_ctx *, int, const char *); -#ifdef ENABLE_IMAGES -static void screen_write_image_damage(struct screen_write_ctx *, u_int, - u_int, u_int, u_int); -#endif static int screen_write_overwrite(struct screen_write_ctx *, struct grid_cell *, u_int); static int screen_write_combine(struct screen_write_ctx *, @@ -396,20 +392,6 @@ screen_write_init(struct screen_write_ctx *ctx, struct screen *s) ctx->bg = 8; } -#ifdef ENABLE_IMAGES -/* Damage temporal images when application input writes text cells. */ -static void -screen_write_image_damage(struct screen_write_ctx *ctx, u_int x, u_int y, - u_int sx, u_int sy) -{ - image_redraw_area(ctx, x, y, sx, sy); - if (ctx->flags & SCREEN_WRITE_INPUT) { - image_grid_remove_overwritten_spans(ctx->s->grid, x, - ctx->s->grid->hsize + y, sx, sy); - } -} -#endif - /* Initialize writing with a pane. */ void screen_write_start_pane(struct screen_write_ctx *ctx, struct window_pane *wp, @@ -1491,11 +1473,7 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) utf8_set(&gc.data, 'E'); #ifdef ENABLE_IMAGES - image_redraw_all(ctx); - if (ctx->flags & SCREEN_WRITE_INPUT) { - image_grid_remove_overwritten_spans(s->grid, 0, s->grid->hsize, - screen_size_x(s), screen_size_y(s)); - } + image_damage_screen(ctx, 0, 0, screen_size_x(s), screen_size_y(s)); #endif for (yy = 0; yy < screen_size_y(s); yy++) { @@ -1540,13 +1518,13 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; + 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 - screen_write_initctx(ctx, &ttyctx, 0, 1); - ttyctx.bg = bg; - grid_view_insert_cells(s->grid, s->cx, s->cy, nx, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1580,13 +1558,13 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; + 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 - screen_write_initctx(ctx, &ttyctx, 0, 1); - ttyctx.bg = bg; - grid_view_delete_cells(s->grid, s->cx, s->cy, nx, bg); screen_write_collect_flush(ctx, 0, __func__); @@ -1620,13 +1598,13 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; -#ifdef ENABLE_IMAGES - image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); -#endif - 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__); @@ -1654,10 +1632,6 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg) if (ny == 0) ny = 1; -#ifdef ENABLE_IMAGES - image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy); -#endif - if (s->cy < s->rupper || s->cy > s->rlower) { if (ny > sy - s->cy) ny = sy - s->cy; @@ -1667,6 +1641,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__); @@ -1691,6 +1669,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 @@ -1721,10 +1704,6 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) if (ny == 0) ny = 1; -#ifdef ENABLE_IMAGES - image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy); -#endif - if (s->cy < s->rupper || s->cy > s->rlower) { if (ny > sy - s->cy) ny = sy - s->cy; @@ -1734,6 +1713,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__); @@ -1760,6 +1743,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 @@ -1790,11 +1777,17 @@ 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, screen_size_x(s), 1); + image_redraw_area(ctx, 0, s->cy, sx, 1); #endif flags = gl->flags & GRID_LINE_OSC133_FLAGS; @@ -1828,11 +1821,20 @@ 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, 0, s->cy, screen_size_x(s), 1); + 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); @@ -1858,7 +1860,7 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg) } #ifdef ENABLE_IMAGES - image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); + image_redraw_area(ctx, 0, s->cy, s->cx + 1, 1); #endif if (s->cx > sx - 1) @@ -1982,11 +1984,7 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg) } #ifdef ENABLE_IMAGES - if (rlower == screen_size_y(s) - 1) - image_redraw_scroll(ctx); - else - image_redraw_area(ctx, 0, rupper, screen_size_x(s), - rlower - rupper); + image_redraw_scroll(ctx); #endif grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg); @@ -2080,13 +2078,15 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; -#ifdef ENABLE_IMAGES - image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy); -#endif - 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 && @@ -2160,13 +2160,14 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; -#ifdef ENABLE_IMAGES - image_redraw_area(ctx, 0, 0, screen_size_x(s), s->cy + 1); -#endif - 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) @@ -2232,13 +2233,13 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; + screen_write_initctx(ctx, &ttyctx, 1, 1); + ttyctx.bg = bg; + #ifdef ENABLE_IMAGES image_redraw_all(ctx); #endif - screen_write_initctx(ctx, &ttyctx, 1, 1); - ttyctx.bg = bg; - /* Scroll into history if it is enabled. */ if ((s->grid->flags & GRID_HISTORY) && ctx->wp != NULL && @@ -2662,14 +2663,19 @@ 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; 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); @@ -2728,7 +2734,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); } @@ -2737,7 +2743,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); } @@ -2747,7 +2753,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) } #ifdef ENABLE_IMAGES - screen_write_image_damage(ctx, s->cx, s->cy, ci->used, 1); + 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, @@ -2760,7 +2766,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); } @@ -2883,8 +2889,8 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) screen_write_initctx(ctx, &ttyctx, 0, 0); #ifdef ENABLE_IMAGES - /* Input semantics decide which image layers this text damages. */ - screen_write_image_damage(ctx, s->cx, s->cy, width, 1); + /* Mark images as damaged. */ + image_damage_screen(ctx, s->cx, s->cy, width, 1); #endif /* Handle overwriting of UTF-8 characters. */ @@ -3118,10 +3124,12 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) } else force_wide = 0; - /* Set the new cell. */ #ifdef ENABLE_IMAGES - screen_write_image_damage(ctx, cx - n, cy, n, 1); + /* 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) grid_view_set_padding(gd, cx - 1, cy, last.bg); @@ -3195,12 +3203,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; } @@ -3218,7 +3226,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; } } @@ -3257,24 +3265,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 image *im; - - im = sixel_to_image(si); - if (im == NULL) { - sixel_free(si); - return; - } - image_write_sixel(ctx, im, bg); - image_free(image_get_id(im)); -} -#endif - /* Turn alternate screen on. */ void screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, diff --git a/tmux-image.h b/tmux-image.h new file mode 100644 index 000000000..aed392518 --- /dev/null +++ b/tmux-image.h @@ -0,0 +1,254 @@ +/* $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 + +struct grid; +struct grid_line; +struct image; +struct image_line; +struct image_store; +struct screen; +struct screen_write_ctx; +struct sixel_image; +struct tty; +struct grid_cell; +struct image_rect; +struct image_backend; + +#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) +#define IMAGE_BACKEND_GRAPHICAL 0x1 +#define IMAGE_BACKEND_SCROLLS 0x2 +#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 { + KITTY_PARSE_ERROR = -1, + KITTY_PARSE_OK, + KITTY_PARSE_MORE, + KITTY_PARSE_MISSING, + KITTY_PARSE_UNSUPPORTED +}; + +/* Reply fields and placement actions returned by one Kitty command. */ +struct kitty_parse_result { + u_int image_id; + u_int image_number; + u_int replace_id; + u_int placement_id; + u_int quiet; + u_int x; + u_int y; + int32_t z; + char action; + char delete; + enum kitty_parse_status status; +}; + +/* Resolved source cell and application IDs of one Unicode placeholder. */ +struct kitty_placeholder { + struct image *image; + u_int source_x; + u_int source_y; + u_int image_id; + u_int placement_id; + int32_t z; +}; + +/* 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 *); +void image_set_no_cursor(struct image *); +int image_has_no_cursor(struct image *); +struct sixel_image *image_get_sixel(struct image *); +void image_set_sixel(struct image *, struct sixel_image *); +u_int image_get_source_id(struct image *); +u_int image_get_order_id(struct image *); +void image_set_order_id(struct image *, u_int); +void *image_get_fallback_data(struct image *); +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_char *image_base64_decode(const char *, size_t, size_t, size_t *); +u_char *image_png_decode(const u_char *, size_t, size_t, u_int *, + u_int *); + +/* 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_remove_overwritten_spans(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 *); +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); +void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *, + u_int, u_int, u_int, u_int); +void image_grid_resize_width(struct grid *, u_int); +int image_grid_has_image(struct grid *, u_int); +int image_grid_get_placeholder(struct grid *, u_int, u_int, + struct kitty_placeholder *); +void image_place_cell_kitty(struct screen_write_ctx *, struct image *, + u_int, u_int, u_int, u_int, u_int, u_int, int32_t); +void image_write_sixel(struct screen_write_ctx *, struct image *, u_int); +void image_write_kitty(struct screen_write_ctx *, struct image *, u_int, + u_int, u_int, int32_t); +void image_clear(struct screen_write_ctx *, u_int); +void image_clear_kitty(struct screen_write_ctx *, + const struct kitty_parse_result *); +int image_get_fallback_at(struct tty *, struct screen *, u_int, u_int, + const struct grid_cell *, struct grid_cell *); +struct image *image_rect_get_image(const struct image_rect *); +void image_rect_get_coords(const struct image_rect *, u_int *, u_int *, + u_int *, u_int *, u_int *, u_int *); +int64_t image_rect_get_z(const struct image_rect *); +void image_draw_line(struct tty *, struct screen *, u_int, u_int, u_int, + u_int, u_int, int); +void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int); +void image_redraw_finish(struct tty *); +void image_draw_flush(struct tty *); +int image_backend_flags(struct tty *); +int image_tty_update(struct tty *); +void image_tty_geometry_changed(struct tty *); +void image_tty_free(struct tty *, int); + +/* image-fallback.c */ +void fallback_get_cell(struct tty *, struct image *, u_int, u_int, + const struct grid_cell *, struct grid_cell *); +void fallback_free(struct image *); + +/* image-kitty.c */ +struct image *kitty_parse_image(void **, const u_char *, size_t, u_int, + u_int, struct kitty_parse_result *); +int kitty_placeholder_to_image(void *, struct grid *, + struct grid_cell *, u_int, u_int, struct kitty_placeholder *); +int kitty_cell_is_placeholder(const struct grid_cell *); +void kitty_delete_images(void *, struct screen_write_ctx *, + const struct kitty_parse_result *); +void kitty_free_state(void *); +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_finish(struct tty *); +void kitty_free_output_state(struct tty *, int); + +/* image-sixel.c */ +#define SIXEL_COLOUR_REGISTERS 1024 +struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int, + u_int); +void sixel_write(struct screen_write_ctx *, struct sixel_image *, + u_int); +void sixel_draw_rect(struct tty *, const struct image_rect *); +void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int); +void sixel_free_output(struct tty *, int); +void sixel_flush_output(struct tty *); +struct image *sixel_to_image(struct sixel_image *); +void sixel_free(struct sixel_image *); +void sixel_log(struct sixel_image *); + +#endif /* ENABLE_IMAGES */ + +#endif /* TMUX_IMAGE_H */ diff --git a/tmux.1 b/tmux.1 index 29c8a601e..c4bb7e2d9 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 @@ -7463,6 +7463,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" @@ -7531,6 +7532,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 f2555b2c0..bffe7b62c 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.1459 2026/10/13 10:21:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -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,15 +80,6 @@ struct screen_write_cline; struct screen_write_ctx; struct session; -#ifdef ENABLE_IMAGES -struct image; -struct image_backend; -struct image_rect; -struct image_line; -struct image_store; -struct sixel_image; -#endif - struct tty_ctx; struct tty_code; struct tty_key; @@ -1048,123 +1042,6 @@ struct style { u_int link; }; -#ifdef ENABLE_IMAGES -/* Private ACS keys used by the text image backend. */ -#define TTY_ACS_IMAGE_SHADE_LIGHT 0x80 -#define TTY_ACS_IMAGE_SHADE_MEDIUM 0x81 -#define TTY_ACS_IMAGE_SHADE_DARK 0x82 -#define TTY_ACS_IMAGE_BLOCK 0x83 -#define TTY_ACS_IMAGE_HALF_UPPER 0x84 -#define TTY_ACS_IMAGE_HALF_LOWER 0x85 -#define TTY_ACS_IMAGE_HALF_LEFT 0x86 -#define TTY_ACS_IMAGE_HALF_RIGHT 0x87 -#define TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT 0x88 -#define TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT 0x89 -#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT 0x8a -#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT 0x8b -#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT 0x8c -#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT 0x8d -#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT 0x8e -#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT 0x8f -#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT 0x90 -#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT 0x91 -#define TTY_ACS_IMAGE_SEXTANT_FIRST 0x92 -#define TTY_ACS_IMAGE_SEXTANT_LAST 0xcd - -/* 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]; -}; -/* Immutable pixels and derived output data shared by image placements. */ -struct image { - u_int id; - u_int references; - u_int flags; - /* Retained parent (zero for sources) and original source IDs. */ - u_int parent_id; - u_int source_id; - u_int order_id; /* Original creation order. */ - /* Pixel dimensions, including the logical transparent canvas. */ - u_int width; - u_int height; - u_int canvas_width; - u_int canvas_height; - /* Grid dimensions and pixel buffer lengths in bytes. */ - u_int sx; - u_int sy; - size_t stride; - size_t size; - u_char *pixels; - struct sixel_image *sixel; - struct image_cell *cells; - void *fallback_data; - - RB_ENTRY(image) entry; -}; -RB_HEAD(images, image); -#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) - -/* Source crop and padded destination canvas for creating an image view. */ -struct image_view { - u_int x; /* Crop origin in source pixels. */ - u_int y; - u_int width; /* Crop size in source pixels. */ - u_int height; - u_int scaled_width; /* Resampled crop size in pixels. */ - u_int scaled_height; - u_int canvas_width; /* Padded canvas size in pixels. */ - u_int canvas_height; - u_int sx; /* Placement size in cells. */ - u_int sy; - u_int x_offset; /* Crop origin in canvas pixels. */ - u_int y_offset; -}; - -enum kitty_parse_status { - KITTY_PARSE_ERROR = -1, - KITTY_PARSE_OK, - KITTY_PARSE_MORE, - KITTY_PARSE_MISSING, - KITTY_PARSE_UNSUPPORTED -}; - -/* Reply fields and placement actions returned by one Kitty command. */ -struct kitty_parse_result { - u_int image_id; - u_int image_number; - u_int replace_id; /* Old server ID, or zero. */ - u_int placement_id; - u_int quiet; - u_int x; /* Deletion coordinates. */ - u_int y; - int32_t z; - char action; - char delete; - enum kitty_parse_status status; -}; - -/* Resolved source cell and application IDs of one Unicode placeholder. */ -struct kitty_placeholder { - struct image *image; - u_int source_x; /* Origin in image cells. */ - u_int source_y; - u_int image_id; - u_int placement_id; - int32_t z; -}; -#endif - /* Cursor style. */ enum screen_cursor_style { SCREEN_CURSOR_DEFAULT, @@ -1493,6 +1370,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; @@ -3764,10 +3642,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 *, @@ -4357,115 +4231,6 @@ void spawn_editor_finish(struct window_pane *); /* regsub.c */ char *regsub(const char *, const char *, const char *, int); -#ifdef ENABLE_IMAGES -/* image.c */ -#define IMAGE_BACKEND_GRAPHICAL 0x1 -#define IMAGE_BACKEND_SCROLLS 0x2 -#define IMAGE_BACKEND_CLIPPED 0x4 - -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 *, const struct image_view *); -struct image *image_find(u_int); -u_int image_get_id(const struct image *); -void image_get_size(const struct image *, u_int *, u_int *); -void image_get_canvas_size(const struct image *, u_int *, u_int *); -void image_get_size_in_cells(const struct image *, u_int *, u_int *); -const u_char *image_get_pixels(const struct image *, size_t *, size_t *); -void image_set_no_cursor(struct image *); -struct sixel_image *image_get_sixel(const struct image *); -void image_set_sixel(struct image *, struct sixel_image *); -void image_ref(u_int); -void image_free(u_int); -void image_write_sixel(struct screen_write_ctx *, struct image *, - u_int); -void image_write_kitty(struct screen_write_ctx *, struct image *, - u_int, u_int, u_int, int32_t); -void image_get_pixel_rect(const struct image *, u_int, u_int, - u_int, u_int, u_int *, u_int *, u_int *, u_int *); -void image_size_in_cells(u_int, u_int, u_int, u_int, u_int *, - u_int *); -u_char *image_base64_decode(const char *, size_t, size_t, size_t *); -u_char *image_png_decode(const u_char *, size_t, size_t, u_int *, - u_int *); -void image_redraw_area(struct screen_write_ctx *, u_int, u_int, - u_int, u_int); -void image_redraw_all(struct screen_write_ctx *); -void image_redraw_scroll(struct screen_write_ctx *); -void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int); -void image_redraw_finish(struct tty *); -void image_draw_flush(struct tty *); -int image_backend_flags(struct tty *); -int image_tty_update(struct tty *); -void image_tty_geometry_changed(struct tty *); -void image_tty_free(struct tty *, int); -void image_draw_line(struct tty *, struct screen *, u_int, u_int, - u_int, u_int, u_int, int); -const struct image_cell *image_get_cell(struct image *, u_int, u_int); -int image_get_fallback_at(struct tty *, struct screen *, u_int, - u_int, const struct grid_cell *, struct grid_cell *); -struct image *image_rect_get_image(const struct image_rect *); -void image_rect_get_coords(const struct image_rect *, - u_int *, u_int *, u_int *, u_int *, u_int *, u_int *); -int64_t image_rect_get_z(const struct image_rect *); -void image_clear(struct screen_write_ctx *, u_int); -void image_clear_kitty(struct screen_write_ctx *, - const struct kitty_parse_result *); -int image_grid_has_image(struct grid *, u_int); -void image_grid_remove_overwritten_spans(struct grid *, u_int, - u_int, u_int, u_int); -void image_grid_free_line(struct grid *, struct grid_line *); -void image_grid_free(struct grid *); -void image_grid_move_cells(struct grid *, u_int, u_int, u_int, - u_int); -void image_grid_duplicate_lines(struct grid *, u_int, struct grid *, - u_int, u_int); -void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *, - u_int, u_int, u_int, u_int); -void image_grid_resize_width(struct grid *, u_int); -int image_grid_line_has_images(const struct grid_line *); -int image_grid_area_has_images(struct grid *, u_int, u_int, u_int, - u_int); -int image_grid_get_placeholder(struct grid *, u_int, u_int, - struct kitty_placeholder *); -void image_place_cell_kitty(struct screen_write_ctx *, struct image *, - u_int, u_int, u_int, u_int, u_int, u_int, int32_t); - -/* image-fallback.c */ -void fallback_get_cell(struct tty *, struct image *, u_int, - u_int, const struct grid_cell *, struct grid_cell *); -void fallback_free(struct image *); - -/* image-kitty.c */ -struct image *kitty_parse_image(void **, const u_char *, size_t, u_int, - u_int, struct kitty_parse_result *); -int kitty_placeholder_to_image(void *, struct grid *, - struct grid_cell *, u_int, u_int, - struct kitty_placeholder *); -int kitty_cell_is_placeholder(const struct grid_cell *); -void kitty_delete_images(void *, struct screen_write_ctx *, - const struct kitty_parse_result *); -void kitty_free_state(void *); -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_finish(struct tty *); -void kitty_free_output_state(struct tty *, int); - -/* image-sixel.c */ -#define SIXEL_COLOUR_REGISTERS 1024 -void sixel_draw_rect(struct tty *, - const struct image_rect *); -void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int); -void sixel_free_output(struct tty *, int); -void sixel_flush_output(struct tty *); -struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int, - u_int); -void sixel_free(struct sixel_image *); -void sixel_log(struct sixel_image *); -struct image *sixel_to_image(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 edec0f69a..0891699a7 100644 --- a/tty.c +++ b/tty.c @@ -1487,7 +1487,6 @@ tty_check_codeset(struct tty *tty, const struct grid_cell *gc) return (&new); } - void tty_sync_start(struct tty *tty) { diff --git a/utf8.c b/utf8.c index 71918083b..4142a7229 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 @@ -835,7 +835,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-tree.c b/window-tree.c index 7e90a5a2f..cbb9a5ba8 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))) { diff --git a/window.c b/window.c index 5a62e44c8..71f8a0c9d 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 @@ -1670,6 +1670,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);