From cb1f41c9ae4a4320ea10250c7d0ae0807c1c0e36 Mon Sep 17 00:00:00 2001 From: a Date: Sun, 6 Sep 2026 20:41:24 -0500 Subject: [PATCH] kitty: add kitty terminal graphics protocol support Adds kitty.c/kitty.h implementing the kitty graphics protocol behind KITTY_GRAPHICS_PATCH, enabled in patches.h. Supports transmission (direct, file, temp file, POSIX shared memory), the RGB/RGBA/PNG formats with a self-contained PNG decoder, zlib compression, chunked transfers, placements with source rects, scaling, cell offsets, z-index ordering with alpha blending via XRender, relative and Unicode-placeholder virtual placements, the full set of delete selectors, queries, image numbers and animation (frame transfer, composition and playback). --- Makefile | 8 +- config.mk | 7 +- kitty.c | 2566 ++++++++++++++++++++++++++++++++++++++++++++++++ kitty.h | 91 ++ patch/reflow.c | 29 + patches.def.h | 14 + patches.h | 14 + st.c | 42 + st.h | 6 + x.c | 19 + 10 files changed, 2791 insertions(+), 5 deletions(-) create mode 100644 kitty.c create mode 100644 kitty.h diff --git a/Makefile b/Makefile index 2b3859a..f8789dd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ include config.mk -SRC = st.c x.c $(LIGATURES_C) $(SIXEL_C) +SRC = st.c x.c $(LIGATURES_C) $(SIXEL_C) $(KITTY_C) OBJ = $(SRC:.c=.o) all: st @@ -18,8 +18,8 @@ patches.h: .c.o: $(CC) $(STCFLAGS) -c $< -st.o: config.h st.h win.h -x.o: arg.h config.h st.h win.h $(LIGATURES_H) +st.o: config.h st.h win.h $(KITTY_H) +x.o: arg.h config.h st.h win.h $(LIGATURES_H) $(KITTY_H) $(OBJ): config.h config.mk patches.h @@ -32,7 +32,7 @@ clean: dist: clean mkdir -p st-$(VERSION) cp -R FAQ LEGACY TODO LICENSE Makefile README config.mk\ - config.def.h st.info st.1 arg.h st.h win.h $(LIGATURES_H) $(SRC)\ + config.def.h st.info st.1 arg.h st.h win.h $(LIGATURES_H) $(KITTY_H) $(SRC)\ st-$(VERSION) tar -cf - st-$(VERSION) | gzip > st-$(VERSION).tar.gz rm -rf st-$(VERSION) diff --git a/config.mk b/config.mk index b020a1a..6646a5c 100644 --- a/config.mk +++ b/config.mk @@ -30,6 +30,11 @@ XRENDER = `$(PKG_CONFIG) --libs xrender` SIXEL_C = sixel.c sixel_hls.c SIXEL_LIBS = `$(PKG_CONFIG) --libs imlib2` +# Uncomment this for the kitty graphics patch / KITTY_GRAPHICS_PATCH +KITTY_C = kitty.c +KITTY_H = kitty.h +KITTY_LIBS = `$(PKG_CONFIG) --libs zlib xrender` + # Uncomment for the netwmicon patch / NETWMICON_PATCH #NETWMICON_LIBS = `$(PKG_CONFIG) --libs gdlib` @@ -38,7 +43,7 @@ INCS = -I$(X11INC) \ `$(PKG_CONFIG) --cflags fontconfig` \ `$(PKG_CONFIG) --cflags freetype2` \ $(LIGATURES_INC) -LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft ${SIXEL_LIBS} ${XRENDER} ${XCURSOR}\ +LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft ${SIXEL_LIBS} ${KITTY_LIBS} ${XRENDER} ${XCURSOR}\ `$(PKG_CONFIG) --libs fontconfig` \ `$(PKG_CONFIG) --libs freetype2` \ $(LIGATURES_LIBS) \ diff --git a/kitty.c b/kitty.c new file mode 100644 index 0000000..9d119ff --- /dev/null +++ b/kitty.c @@ -0,0 +1,2566 @@ +/* kitty.c - the kitty terminal graphics protocol for st + * + * https://sw.kovidgoyal.net/kitty/graphics-protocol/ + * + * Implements: transmission (direct/file/temp file/shared memory), formats + * RGB/RGBA/PNG, zlib compression, chunked transfer, placements (including + * relative and virtual/Unicode-placeholder placements), deletion, queries, + * image numbers, animation (frame transfer, composition and playback) and + * z-index ordering with alpha blending. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "st.h" +#include "win.h" +#include "kitty.h" + +#if KITTY_GRAPHICS_PATCH + +/* from x.c / st.c */ +extern DC dc; + +/* these live in st.c, repeated here since kitty.c is a separate unit */ +#define IS_SET(flag) ((term.mode & (flag)) != 0) +#define MODE_ALTSCREEN (1 << 2) +#define CURSOR_WRAPNEXT 1 + +#if REFLOW_PATCH +#include "patch/reflow.h" +#elif SCROLLBACK_PATCH +#include "patch/scrollback.h" +#endif + +#define KMIN(a, b) ((a) < (b) ? (a) : (b)) +#define KMAX(a, b) ((a) < (b) ? (b) : (a)) + +/* ------------------------------------------------------------------ */ +/* state */ +/* ------------------------------------------------------------------ */ + +static KittyImage *images; /* all stored images */ +static KittyPlacement *placements[2]; /* [0] main screen, [1] alt screen */ +static size_t storage_used; +static uint32_t last_autoid = 0; + +/* the command currently being assembled from chunks */ +typedef struct { + char a; /* action */ + int f; /* format */ + char t; /* medium */ + int s, v; /* width, height */ + long S, O; /* size, offset */ + uint32_t i; /* image id */ + uint32_t I; /* image number */ + uint32_t p; /* placement id */ + int o; /* compression ('z' or 0) */ + int m; /* more chunks */ + int q; /* quiet */ + int N; /* usage hints */ + int x, y, w, h; /* source rect / frame rect */ + int X, Y; /* cell offsets / composition src / frame mode */ + int c, r; /* cols, rows / frame numbers */ + int C; /* cursor movement policy / compose op */ + int U; /* virtual placement */ + int z; /* z-index / frame gap */ + uint32_t P, Q; /* parent image / placement */ + int H, V; /* relative offsets */ + int d; /* delete target */ + int anim_state; /* s key for a=a */ + int loops; /* v key for a=a */ + /* which keys were seen */ + int has_i, has_I, has_s, has_v, has_w, has_h, has_c, has_r, has_z; + int has_x, has_y, has_X, has_Y, has_p, has_C; +} KittyCmd; + +static struct { + int active; /* a chunked transfer is in progress */ + KittyCmd cmd; /* the command from the first chunk */ + char *buf; /* accumulated base64-decoded payload */ + size_t len, siz; +} chunk; + +/* pending Unicode placeholder state (for diacritic accumulation) */ +static struct { + int active; + int ndia; + uint32_t row, col, msb; +} ph; + +/* ------------------------------------------------------------------ */ +/* small helpers */ +/* ------------------------------------------------------------------ */ + +static uint64_t +kitty_now(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +/* borderpx is static in config.h, so derive the border from the window size */ +static void +kitty_border(int *bx, int *by) +{ + #if ANYSIZE_PATCH + *bx = win.hborderpx; + *by = win.vborderpx; + #else + *bx = (win.w - win.tw) / 2; + *by = (win.h - win.th) / 2; + #endif +} + +static int +kitty_scr(void) +{ + #if REFLOW_PATCH || SCROLLBACK_PATCH + return IS_SET(MODE_ALTSCREEN) ? 0 : term.scr; + #else + return 0; + #endif +} + +static void +kitty_respond(const KittyCmd *cmd, int ok, const char *msg) +{ + char buf[512]; + int n; + + if (cmd->q >= 2 || (ok && cmd->q >= 1)) + return; + /* responses are only sent when the client identified the image */ + if (!cmd->i && !cmd->I && cmd->a != 'q') + return; + + n = snprintf(buf, sizeof buf, "\033_G"); + if (cmd->i) + n += snprintf(buf+n, sizeof buf - n, "i=%u", cmd->i); + if (cmd->I) + n += snprintf(buf+n, sizeof buf - n, "%sI=%u", cmd->i ? "," : "", cmd->I); + if (cmd->p) + n += snprintf(buf+n, sizeof buf - n, ",p=%u", cmd->p); + n += snprintf(buf+n, sizeof buf - n, ";%s\033\\", msg); + ttywrite(buf, n, 0); +} + +/* base64 decoding, ignores whitespace, tolerates missing padding */ +static const signed char b64tab[256] = { + ['A']= 0,['B']= 1,['C']= 2,['D']= 3,['E']= 4,['F']= 5,['G']= 6,['H']= 7, + ['I']= 8,['J']= 9,['K']=10,['L']=11,['M']=12,['N']=13,['O']=14,['P']=15, + ['Q']=16,['R']=17,['S']=18,['T']=19,['U']=20,['V']=21,['W']=22,['X']=23, + ['Y']=24,['Z']=25,['a']=26,['b']=27,['c']=28,['d']=29,['e']=30,['f']=31, + ['g']=32,['h']=33,['i']=34,['j']=35,['k']=36,['l']=37,['m']=38,['n']=39, + ['o']=40,['p']=41,['q']=42,['r']=43,['s']=44,['t']=45,['u']=46,['v']=47, + ['w']=48,['x']=49,['y']=50,['z']=51,['0']=52,['1']=53,['2']=54,['3']=55, + ['4']=56,['5']=57,['6']=58,['7']=59,['8']=60,['9']=61,['+']=62,['/']=63, +}; + +static size_t +kitty_b64decode(const char *src, size_t srclen, char *dst) +{ + size_t i, o = 0; + int acc = 0, bits = 0; + unsigned char c; + + for (i = 0; i < srclen; i++) { + c = (unsigned char)src[i]; + if (c == '=' ) + break; + if (c == '\n' || c == '\r' || c == ' ' || c == '\t') + continue; + if (!b64tab[c] && c != 'A') + continue; + acc = (acc << 6) | b64tab[c]; + bits += 6; + if (bits >= 8) { + bits -= 8; + dst[o++] = (char)((acc >> bits) & 0xff); + } + } + return o; +} + +/* zlib (RFC1950) inflate into a freshly allocated buffer */ +static unsigned char * +kitty_inflate(const unsigned char *src, size_t srclen, size_t *outlen, size_t hint) +{ + z_stream zs; + unsigned char *out, *tmp; + size_t cap = hint ? hint + 64 : (srclen * 4 + 4096); + + memset(&zs, 0, sizeof zs); + if (inflateInit(&zs) != Z_OK) + return NULL; + if (!(out = malloc(cap))) { + inflateEnd(&zs); + return NULL; + } + zs.next_in = (Bytef *)src; + zs.avail_in = srclen; + zs.next_out = out; + zs.avail_out = cap; + + for (;;) { + int r = inflate(&zs, Z_NO_FLUSH); + if (r == Z_STREAM_END) + break; + if (r != Z_OK) { + inflateEnd(&zs); + free(out); + return NULL; + } + if (zs.avail_out == 0) { + if (cap > (SIZE_MAX / 2)) { + inflateEnd(&zs); + free(out); + return NULL; + } + if (!(tmp = realloc(out, cap * 2))) { + inflateEnd(&zs); + free(out); + return NULL; + } + out = tmp; + zs.next_out = out + cap; + zs.avail_out = cap; + cap *= 2; + } + } + *outlen = zs.total_out; + inflateEnd(&zs); + return out; +} + +/* ------------------------------------------------------------------ */ +/* PNG decoding (RGBA8 output) */ +/* ------------------------------------------------------------------ */ + +static uint32_t +kitty_be32(const unsigned char *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +static int +kitty_paeth(int a, int b, int c) +{ + int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); + if (pa <= pb && pa <= pc) return a; + if (pb <= pc) return b; + return c; +} + +/* unfilter a scanline in place; bpp is bytes per pixel (>=1) */ +static void +kitty_unfilter(int filter, unsigned char *cur, const unsigned char *prev, + size_t len, int bpp) +{ + size_t i; + + switch (filter) { + case 0: + break; + case 1: + for (i = bpp; i < len; i++) + cur[i] = (unsigned char)(cur[i] + cur[i-bpp]); + break; + case 2: + if (prev) + for (i = 0; i < len; i++) + cur[i] = (unsigned char)(cur[i] + prev[i]); + break; + case 3: + for (i = 0; i < len; i++) { + int a = (i >= (size_t)bpp) ? cur[i-bpp] : 0; + int b = prev ? prev[i] : 0; + cur[i] = (unsigned char)(cur[i] + ((a + b) >> 1)); + } + break; + case 4: + for (i = 0; i < len; i++) { + int a = (i >= (size_t)bpp) ? cur[i-bpp] : 0; + int b = prev ? prev[i] : 0; + int c = (prev && i >= (size_t)bpp) ? prev[i-bpp] : 0; + cur[i] = (unsigned char)(cur[i] + kitty_paeth(a, b, c)); + } + break; + } +} + +typedef struct { + int w, h, depth, color, interlace; + unsigned char pal[256*3]; + unsigned char trns[256]; + int npal, ntrns; + int has_trnskey; + uint16_t trnskey[3]; +} PngHdr; + +static int +kitty_png_channels(int color) +{ + switch (color) { + case 0: return 1; /* gray */ + case 2: return 3; /* rgb */ + case 3: return 1; /* palette */ + case 4: return 2; /* gray+alpha */ + case 6: return 4; /* rgba */ + } + return -1; +} + +static unsigned +kitty_png_sample(const unsigned char *row, int depth, int idx) +{ + switch (depth) { + case 1: return (row[idx >> 3] >> (7 - (idx & 7))) & 1; + case 2: return (row[idx >> 2] >> (6 - 2*(idx & 3))) & 3; + case 4: return (row[idx >> 1] >> (4 - 4*(idx & 1))) & 15; + case 8: return row[idx]; + case 16: return ((unsigned)row[2*idx] << 8) | row[2*idx+1]; + } + return 0; +} + +/* expand one unfiltered scanline into RGBA8 at dst */ +static void +kitty_png_expand(const PngHdr *hdr, const unsigned char *row, int npix, + unsigned char *dst) +{ + int nch = kitty_png_channels(hdr->color); + int maxval = (1 << (hdr->depth > 8 ? 8 : hdr->depth)) - 1; + int i, c; + unsigned s[4]; + + for (i = 0; i < npix; i++) { + for (c = 0; c < nch; c++) + s[c] = kitty_png_sample(row, hdr->depth, i*nch + c); + if (hdr->depth == 16) + for (c = 0; c < nch; c++) + s[c] >>= 8; + + switch (hdr->color) { + case 0: /* gray */ + case 4: /* gray + alpha */ + { + unsigned g = hdr->depth < 8 ? s[0] * 255 / maxval : s[0]; + dst[4*i+0] = dst[4*i+1] = dst[4*i+2] = (unsigned char)g; + dst[4*i+3] = (hdr->color == 4) ? (unsigned char)s[1] : 255; + if (hdr->color == 0 && hdr->has_trnskey && + kitty_png_sample(row, hdr->depth, i) == hdr->trnskey[0]) + dst[4*i+3] = 0; + break; + } + case 2: /* rgb */ + case 6: /* rgba */ + dst[4*i+0] = (unsigned char)s[0]; + dst[4*i+1] = (unsigned char)s[1]; + dst[4*i+2] = (unsigned char)s[2]; + dst[4*i+3] = (hdr->color == 6) ? (unsigned char)s[3] : 255; + if (hdr->color == 2 && hdr->has_trnskey) { + unsigned r = kitty_png_sample(row, hdr->depth, 3*i+0); + unsigned g = kitty_png_sample(row, hdr->depth, 3*i+1); + unsigned b = kitty_png_sample(row, hdr->depth, 3*i+2); + if (r == hdr->trnskey[0] && g == hdr->trnskey[1] && + b == hdr->trnskey[2]) + dst[4*i+3] = 0; + } + break; + case 3: /* palette */ + { + unsigned idx = s[0]; + if ((int)idx < hdr->npal) { + dst[4*i+0] = hdr->pal[3*idx+0]; + dst[4*i+1] = hdr->pal[3*idx+1]; + dst[4*i+2] = hdr->pal[3*idx+2]; + } else { + dst[4*i+0] = dst[4*i+1] = dst[4*i+2] = 0; + } + dst[4*i+3] = ((int)idx < hdr->ntrns) ? hdr->trns[idx] : 255; + break; + } + } + } +} + +static const int adam7_xo[7] = { 0, 4, 0, 2, 0, 1, 0 }; +static const int adam7_yo[7] = { 0, 0, 4, 0, 2, 0, 1 }; +static const int adam7_xs[7] = { 8, 8, 4, 4, 2, 2, 1 }; +static const int adam7_ys[7] = { 8, 8, 8, 4, 4, 2, 2 }; + +static unsigned char * +kitty_png_decode(const unsigned char *data, size_t len, int *ow, int *oh) +{ + PngHdr hdr; + const unsigned char sig[8] = { 137, 'P', 'N', 'G', '\r', '\n', 26, '\n' }; + unsigned char *idat = NULL, *raw = NULL, *out = NULL; + unsigned char *cur = NULL, *prev = NULL, *rgba = NULL; + size_t idatlen = 0, idatcap = 0, rawlen = 0, pos = 8; + int bpp, nch, pass, ok = 0; + + if (len < 8 || memcmp(data, sig, 8)) + return NULL; + memset(&hdr, 0, sizeof hdr); + + while (pos + 8 <= len) { + uint32_t clen = kitty_be32(data + pos); + const unsigned char *ctype = data + pos + 4; + const unsigned char *cdata = data + pos + 8; + + if (clen > len || pos + 12 + clen > len) + break; + + if (!memcmp(ctype, "IHDR", 4) && clen >= 13) { + hdr.w = (int)kitty_be32(cdata); + hdr.h = (int)kitty_be32(cdata + 4); + hdr.depth = cdata[8]; + hdr.color = cdata[9]; + hdr.interlace = cdata[12]; + if (hdr.w <= 0 || hdr.h <= 0 || + hdr.w > KITTY_MAX_DIM || hdr.h > KITTY_MAX_DIM || + kitty_png_channels(hdr.color) < 0 || + cdata[10] != 0 || cdata[11] != 0) + goto done; + if (hdr.depth != 1 && hdr.depth != 2 && hdr.depth != 4 && + hdr.depth != 8 && hdr.depth != 16) + goto done; + } else if (!memcmp(ctype, "PLTE", 4)) { + hdr.npal = KMIN((int)(clen / 3), 256); + memcpy(hdr.pal, cdata, hdr.npal * 3); + } else if (!memcmp(ctype, "tRNS", 4)) { + if (hdr.color == 3) { + hdr.ntrns = KMIN((int)clen, 256); + memcpy(hdr.trns, cdata, hdr.ntrns); + } else if (hdr.color == 0 && clen >= 2) { + hdr.has_trnskey = 1; + hdr.trnskey[0] = (cdata[0] << 8) | cdata[1]; + if (hdr.depth <= 8) + hdr.trnskey[0] &= (1 << hdr.depth) - 1; + } else if (hdr.color == 2 && clen >= 6) { + hdr.has_trnskey = 1; + hdr.trnskey[0] = (cdata[0] << 8) | cdata[1]; + hdr.trnskey[1] = (cdata[2] << 8) | cdata[3]; + hdr.trnskey[2] = (cdata[4] << 8) | cdata[5]; + } + } else if (!memcmp(ctype, "IDAT", 4)) { + if (idatlen + clen > idatcap) { + size_t ncap = KMAX(idatcap * 2, idatlen + clen + 4096); + unsigned char *t = realloc(idat, ncap); + if (!t) + goto done; + idat = t; + idatcap = ncap; + } + memcpy(idat + idatlen, cdata, clen); + idatlen += clen; + } else if (!memcmp(ctype, "IEND", 4)) { + break; + } + pos += 12 + clen; + } + + if (!idat || hdr.w <= 0) + goto done; + if (hdr.color == 3 && hdr.npal == 0) + goto done; + + nch = kitty_png_channels(hdr.color); + bpp = KMAX(1, (nch * hdr.depth) / 8); + + raw = kitty_inflate(idat, idatlen, &rawlen, 0); + if (!raw) + goto done; + + if ((size_t)hdr.w * hdr.h > (size_t)KITTY_MAX_DIM * KITTY_MAX_DIM) + goto done; + out = calloc((size_t)hdr.w * hdr.h, 4); + if (!out) + goto done; + + { + size_t maxrow = ((size_t)hdr.w * nch * hdr.depth + 7) / 8; + cur = malloc(maxrow + 8); + prev = calloc(maxrow + 8, 1); + rgba = malloc((size_t)hdr.w * 4); + if (!cur || !prev || !rgba) + goto done; + } + + pos = 0; + for (pass = 0; pass < (hdr.interlace ? 7 : 1); pass++) { + int px0 = hdr.interlace ? adam7_xo[pass] : 0; + int py0 = hdr.interlace ? adam7_yo[pass] : 0; + int pxs = hdr.interlace ? adam7_xs[pass] : 1; + int pys = hdr.interlace ? adam7_ys[pass] : 1; + int pw = hdr.interlace ? (hdr.w - px0 + pxs - 1) / pxs : hdr.w; + int phh = hdr.interlace ? (hdr.h - py0 + pys - 1) / pys : hdr.h; + size_t rowbytes; + int yy, xx; + + if (pw <= 0 || phh <= 0) + continue; + rowbytes = ((size_t)pw * nch * hdr.depth + 7) / 8; + memset(prev, 0, rowbytes); + + for (yy = 0; yy < phh; yy++) { + if (pos + 1 + rowbytes > rawlen) + goto done; + memcpy(cur, raw + pos + 1, rowbytes); + kitty_unfilter(raw[pos], cur, yy ? prev : NULL, rowbytes, bpp); + pos += 1 + rowbytes; + + kitty_png_expand(&hdr, cur, pw, rgba); + for (xx = 0; xx < pw; xx++) { + int dx = px0 + xx * pxs, dy = py0 + yy * pys; + if (dx < hdr.w && dy < hdr.h) + memcpy(out + ((size_t)dy * hdr.w + dx) * 4, + rgba + xx * 4, 4); + } + memcpy(prev, cur, rowbytes); + } + } + + ok = 1; + *ow = hdr.w; + *oh = hdr.h; +done: + free(idat); + free(raw); + free(cur); + free(prev); + free(rgba); + if (!ok) { + free(out); + return NULL; + } + return out; +} + +/* ------------------------------------------------------------------ */ +/* image/placement storage */ +/* ------------------------------------------------------------------ */ + +static void +kitty_freepixmap(KittyPlacement *p) +{ + if (p->pixmap) { + XFreePixmap(xw.dpy, (Drawable)p->pixmap); + p->pixmap = NULL; + } +} + +static void kitty_delete_image(KittyImage *im); + +static void +kitty_delete_placement(KittyPlacement *p) +{ + KittyPlacement *q, *next; + KittyImage *im = p->image; + int s; + + /* delete children of this placement first */ + for (s = 0; s < 2; s++) { + for (q = placements[s]; q; q = next) { + next = q->next; + if (q != p && q->parent_img && im && + q->parent_img == im->id && q->parent_plc == p->id) + kitty_delete_placement(q); + } + } + + s = p->alt ? 1 : 0; + if (p->prev) + p->prev->next = p->next; + else + placements[s] = p->next; + if (p->next) + p->next->prev = p->prev; + kitty_freepixmap(p); + if (im && im->refs) + im->refs--; + free(p); +} + +static void +kitty_free_frames(KittyImage *im) +{ + KittyFrame *f, *next; + + for (f = im->frames; f; f = next) { + next = f->next; + if (f->pixels != im->pixels) + free(f->pixels); + free(f); + } + im->frames = NULL; + im->nframes = 0; +} + +static void +kitty_delete_image(KittyImage *im) +{ + KittyPlacement *p, *next; + int s; + + for (s = 0; s < 2; s++) { + for (p = placements[s]; p; p = next) { + next = p->next; + if (p->image == im) + kitty_delete_placement(p); + } + } + + if (im->prev) + im->prev->next = im->next; + else + images = im->next; + if (im->next) + im->next->prev = im->prev; + + if (storage_used >= im->datasize) + storage_used -= im->datasize; + else + storage_used = 0; + kitty_free_frames(im); + free(im->pixels); + free(im); +} + +static KittyImage * +kitty_find_id(uint32_t id) +{ + KittyImage *im; + + if (!id) + return NULL; + for (im = images; im; im = im->next) + if (im->id == id) + return im; + return NULL; +} + +/* newest image with the given number */ +static KittyImage * +kitty_find_number(uint32_t num) +{ + KittyImage *im, *found = NULL; + + if (!num) + return NULL; + for (im = images; im; im = im->next) + if (im->number == num) + found = im; /* list is in creation order; last == newest */ + return found; +} + +static uint32_t +kitty_new_id(void) +{ + uint32_t id; + + for (id = last_autoid + 1; ; id++) { + if (id == 0 || id > 4294967290u) + id = 1; + if (!kitty_find_id(id)) { + last_autoid = id; + return id; + } + } +} + +/* evict images without placements until we are under quota */ +static void +kitty_enforce_quota(void) +{ + KittyImage *im, *next, *victim; + + while (storage_used > KITTY_STORAGE_QUOTA) { + victim = NULL; + /* prefer transient images, then the oldest unreferenced image */ + for (im = images; im; im = im->next) { + if (im->refs) + continue; + if (im->transient) { + victim = im; + break; + } + if (!victim) + victim = im; + } + if (!victim) { + /* everything is referenced, drop the oldest image */ + victim = images; + if (!victim) + return; + } + next = victim->next; + kitty_delete_image(victim); + (void)next; + } +} + +static void +kitty_add_image(KittyImage *im) +{ + KittyImage *tail; + + im->next = NULL; + if (!images) { + im->prev = NULL; + images = im; + } else { + for (tail = images; tail->next; tail = tail->next); + tail->next = im; + im->prev = tail; + } + storage_used += im->datasize; + kitty_enforce_quota(); +} + +static KittyFrame * +kitty_get_frame(KittyImage *im, int n) +{ + KittyFrame *f; + + if (n <= 0) + return NULL; + for (f = im->frames; f; f = f->next) + if (--n == 0) + return f; + return NULL; +} + +/* make sure the root frame exists, referencing im->pixels */ +static void +kitty_ensure_rootframe(KittyImage *im) +{ + KittyFrame *f; + + if (im->frames) + return; + if (!(f = calloc(1, sizeof *f))) + return; + f->pixels = im->pixels; + f->width = im->width; + f->height = im->height; + f->gap = 0; + im->frames = f; + im->nframes = 1; + im->current_frame = 1; +} + +static const unsigned char * +kitty_frame_pixels(KittyImage *im, int n) +{ + KittyFrame *f = kitty_get_frame(im, n); + + if (f && f->pixels) + return f->pixels; + return im->pixels; +} + +/* ------------------------------------------------------------------ */ +/* placements */ +/* ------------------------------------------------------------------ */ + +static KittyPlacement * +kitty_find_placement(KittyImage *im, uint32_t pid) +{ + KittyPlacement *p; + int s; + + for (s = 0; s < 2; s++) + for (p = placements[s]; p; p = p->next) + if (p->image == im && p->id == pid) + return p; + return NULL; +} + +static void +kitty_add_placement(KittyPlacement *p) +{ + int s = p->alt ? 1 : 0; + KittyPlacement *tail; + + p->next = NULL; + if (!placements[s]) { + p->prev = NULL; + placements[s] = p; + } else { + for (tail = placements[s]; tail->next; tail = tail->next); + tail->next = p; + p->prev = tail; + } + if (p->image) + p->image->refs++; +} + +/* resolve the on-screen origin of a placement, following parents */ +static int +kitty_resolve_pos(KittyPlacement *p, int *ox, int *oy, int depth) +{ + KittyImage *pim; + KittyPlacement *parent; + + if (!p->parent_img) { + *ox = p->x; + *oy = p->y; + return 1; + } + if (depth > KITTY_MAX_REL_DEPTH) + return 0; + pim = kitty_find_id(p->parent_img); + if (!pim) + return 0; + parent = kitty_find_placement(pim, p->parent_plc); + if (!parent) { + /* pick any placement of the parent image */ + KittyPlacement *q; + int s; + for (s = 0; s < 2 && !parent; s++) + for (q = placements[s]; q; q = q->next) + if (q->image == pim) { + parent = q; + break; + } + } + if (!parent) + return 0; + if (!kitty_resolve_pos(parent, ox, oy, depth+1)) + return 0; + *ox += p->hoff; + *oy += p->voff; + return 1; +} + +static int +kitty_would_cycle(uint32_t img, uint32_t plc, uint32_t pimg, uint32_t pplc, int depth) +{ + KittyImage *im; + KittyPlacement *p; + + if (depth > KITTY_MAX_REL_DEPTH + 1) + return 1; + if (pimg == img && pplc == plc) + return 1; + im = kitty_find_id(pimg); + if (!im) + return 0; + p = kitty_find_placement(im, pplc); + if (!p || !p->parent_img) + return 0; + return kitty_would_cycle(img, plc, p->parent_img, p->parent_plc, depth+1); +} + +/* ------------------------------------------------------------------ */ +/* command parsing */ +/* ------------------------------------------------------------------ */ + +static void +kitty_cmd_defaults(KittyCmd *c) +{ + memset(c, 0, sizeof *c); + c->a = 't'; + c->f = 32; + c->t = 'd'; + c->d = 'a'; + c->z = 0; +} + +static long +kitty_atol(const char *s, const char *end) +{ + long v = 0; + int neg = 0; + + if (s < end && (*s == '-' || *s == '+')) { + neg = (*s == '-'); + s++; + } + for (; s < end && *s >= '0' && *s <= '9'; s++) { + if (v > (LONG_MAX - 9) / 10) + return neg ? LONG_MIN : LONG_MAX; + v = v * 10 + (*s - '0'); + } + return neg ? -v : v; +} + +/* parse the control data; returns pointer to the payload (after ';') */ +static const char * +kitty_parse(const char *buf, size_t len, KittyCmd *c) +{ + const char *p = buf, *end = buf + len, *payload = end; + const char *semi = memchr(buf, ';', len); + + kitty_cmd_defaults(c); + if (semi) { + end = semi; + payload = semi + 1; + } + + while (p < end) { + const char *eq, *comma; + char key; + long val; + + comma = memchr(p, ',', end - p); + if (!comma) + comma = end; + eq = memchr(p, '=', comma - p); + if (!eq || eq == p) { + p = comma + 1; + continue; + } + key = *p; + val = kitty_atol(eq + 1, comma); + + switch (key) { + case 'a': c->a = eq[1]; break; + case 'd': c->d = eq[1]; break; + case 't': c->t = eq[1]; break; + case 'o': c->o = (eq[1] == 'z') ? 'z' : 0; break; + case 'f': c->f = (int)val; break; + case 's': c->s = (int)val; c->has_s = 1; c->anim_state = (int)val; break; + case 'v': c->v = (int)val; c->has_v = 1; c->loops = (int)val; break; + case 'S': c->S = val; break; + case 'O': c->O = val; break; + case 'i': c->i = (uint32_t)val; c->has_i = 1; break; + case 'I': c->I = (uint32_t)val; c->has_I = 1; break; + case 'p': c->p = (uint32_t)val; c->has_p = 1; break; + case 'm': c->m = (int)val; break; + case 'q': c->q = (int)val; break; + case 'N': c->N = (int)val; break; + case 'x': c->x = (int)val; c->has_x = 1; break; + case 'y': c->y = (int)val; c->has_y = 1; break; + case 'w': c->w = (int)val; c->has_w = 1; break; + case 'h': c->h = (int)val; c->has_h = 1; break; + case 'X': c->X = (int)val; c->has_X = 1; break; + case 'Y': c->Y = (int)val; c->has_Y = 1; break; + case 'c': c->c = (int)val; c->has_c = 1; break; + case 'r': c->r = (int)val; c->has_r = 1; break; + case 'C': c->C = (int)val; c->has_C = 1; break; + case 'U': c->U = (int)val; break; + case 'z': c->z = (int)val; c->has_z = 1; break; + case 'P': c->P = (uint32_t)val; break; + case 'Q': c->Q = (uint32_t)val; break; + case 'H': c->H = (int)val; break; + case 'V': c->V = (int)val; break; + } + p = comma + 1; + } + return payload; +} + +/* ------------------------------------------------------------------ */ +/* data loading */ +/* ------------------------------------------------------------------ */ + +static int +kitty_path_is_temp(const char *path) +{ + const char *tmpdir = getenv("TMPDIR"); + + if (!strstr(path, "tty-graphics-protocol")) + return 0; + if (!strncmp(path, "/tmp/", 5) || !strncmp(path, "/dev/shm/", 9) || + !strncmp(path, "/var/tmp/", 9)) + return 1; + if (tmpdir && *tmpdir && !strncmp(path, tmpdir, strlen(tmpdir))) + return 1; + return 0; +} + +static int +kitty_path_forbidden(const char *path) +{ + static const char *bad[] = { "/proc/", "/sys/", "/dev/" }; + size_t i; + + for (i = 0; i < sizeof bad / sizeof *bad; i++) + if (!strncmp(path, bad[i], strlen(bad[i]))) { + /* /dev/shm is allowed as a temp location */ + if (!strncmp(path, "/dev/shm/", 9)) + continue; + return 1; + } + return 0; +} + +/* read the transmission medium into a malloc'd buffer */ +static unsigned char * +kitty_read_medium(const KittyCmd *cmd, const char *payload, size_t plen, + size_t *outlen, const char **err) +{ + unsigned char *data = NULL; + char path[4096]; + size_t n; + + *err = NULL; + + if (cmd->t == 'd') { + if (!(data = malloc(plen ? plen : 1))) { + *err = "ENOMEM:out of memory"; + return NULL; + } + memcpy(data, payload, plen); + *outlen = plen; + return data; + } + + n = KMIN(plen, sizeof path - 1); + memcpy(path, payload, n); + path[n] = '\0'; + + if (cmd->t == 's') { + int fd; + struct stat st; + void *map; + size_t off = (size_t)KMAX(cmd->O, 0); + size_t want; + + fd = shm_open(path, O_RDONLY, 0); + if (fd < 0) { + *err = "ENOENT:shared memory object not found"; + return NULL; + } + if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { + close(fd); + *err = "EBADF:invalid shared memory object"; + return NULL; + } + want = cmd->S > 0 ? (size_t)cmd->S : (size_t)st.st_size - KMIN(off, (size_t)st.st_size); + if (off + want > (size_t)st.st_size) + want = (size_t)st.st_size > off ? (size_t)st.st_size - off : 0; + map = mmap(NULL, off + want, PROT_READ, MAP_SHARED, fd, 0); + close(fd); + if (map == MAP_FAILED) { + shm_unlink(path); + *err = "EIO:could not map shared memory"; + return NULL; + } + if ((data = malloc(want ? want : 1))) + memcpy(data, (char *)map + off, want); + munmap(map, off + want); + shm_unlink(path); + if (!data) { + *err = "ENOMEM:out of memory"; + return NULL; + } + *outlen = want; + return data; + } + + if (cmd->t == 'f' || cmd->t == 't') { + FILE *fp; + struct stat st; + size_t want, got; + + if (kitty_path_forbidden(path)) { + *err = "EPERM:refusing to read from this location"; + return NULL; + } + if (stat(path, &st) < 0) { + *err = "ENOENT:file not found"; + return NULL; + } + if (!S_ISREG(st.st_mode)) { + *err = "EINVAL:not a regular file"; + return NULL; + } + if (!(fp = fopen(path, "rb"))) { + *err = "EIO:could not open file"; + return NULL; + } + if (cmd->O > 0 && fseek(fp, cmd->O, SEEK_SET) < 0) { + fclose(fp); + *err = "EIO:could not seek in file"; + return NULL; + } + want = cmd->S > 0 ? (size_t)cmd->S : (size_t)st.st_size; + if (want > (size_t)st.st_size) + want = (size_t)st.st_size; + if (!(data = malloc(want ? want : 1))) { + fclose(fp); + *err = "ENOMEM:out of memory"; + return NULL; + } + got = fread(data, 1, want, fp); + fclose(fp); + if (cmd->t == 't' && kitty_path_is_temp(path)) + unlink(path); + *outlen = got; + return data; + } + + *err = "EINVAL:unknown transmission medium"; + return NULL; +} + +/* decode payload data into RGBA pixels */ +static unsigned char * +kitty_decode(const KittyCmd *cmd, unsigned char *data, size_t len, + int *w, int *h, const char **err) +{ + unsigned char *pixels = NULL; + unsigned char *decomp = NULL; + size_t need; + int i; + + *err = NULL; + + if (cmd->o == 'z') { + size_t dlen = 0; + size_t hint = 0; + if (cmd->f == 24 && cmd->s > 0 && cmd->v > 0) + hint = (size_t)cmd->s * cmd->v * 3; + else if (cmd->f == 32 && cmd->s > 0 && cmd->v > 0) + hint = (size_t)cmd->s * cmd->v * 4; + decomp = kitty_inflate(data, len, &dlen, hint); + if (!decomp) { + *err = "EINVAL:could not decompress data"; + return NULL; + } + data = decomp; + len = dlen; + } + + if (cmd->f == 100) { + pixels = kitty_png_decode(data, len, w, h); + if (!pixels) + *err = "EINVAL:could not decode PNG data"; + free(decomp); + return pixels; + } + + if (cmd->f != 24 && cmd->f != 32) { + *err = "EINVAL:unsupported image format"; + free(decomp); + return NULL; + } + if (cmd->s <= 0 || cmd->v <= 0 || + cmd->s > KITTY_MAX_DIM || cmd->v > KITTY_MAX_DIM) { + *err = "EINVAL:invalid image dimensions"; + free(decomp); + return NULL; + } + + *w = cmd->s; + *h = cmd->v; + need = (size_t)cmd->s * cmd->v * (cmd->f == 24 ? 3 : 4); + if (len < need) { + *err = "EINVAL:insufficient image data"; + free(decomp); + return NULL; + } + if (!(pixels = malloc((size_t)cmd->s * cmd->v * 4))) { + *err = "ENOMEM:out of memory"; + free(decomp); + return NULL; + } + if (cmd->f == 32) { + memcpy(pixels, data, need); + } else { + for (i = 0; i < cmd->s * cmd->v; i++) { + pixels[4*i+0] = data[3*i+0]; + pixels[4*i+1] = data[3*i+1]; + pixels[4*i+2] = data[3*i+2]; + pixels[4*i+3] = 255; + } + } + free(decomp); + return pixels; +} + +/* ------------------------------------------------------------------ */ +/* actions */ +/* ------------------------------------------------------------------ */ + +static void kitty_do_display(const KittyCmd *cmd, KittyImage *im); + +/* a=t / a=T / a=q */ +static void +kitty_do_transmit(const KittyCmd *cmd, const char *payload, size_t plen) +{ + unsigned char *data, *pixels; + size_t datalen = 0; + const char *err = NULL; + int w = 0, h = 0; + KittyImage *im; + KittyCmd resp = *cmd; + + if (cmd->has_i && cmd->has_I && cmd->i && cmd->I) { + kitty_respond(cmd, 0, "EINVAL:both i and I specified"); + return; + } + + data = kitty_read_medium(cmd, payload, plen, &datalen, &err); + if (!data) { + kitty_respond(cmd, 0, err ? err : "EINVAL:could not read data"); + return; + } + pixels = kitty_decode(cmd, data, datalen, &w, &h, &err); + free(data); + if (!pixels) { + kitty_respond(cmd, 0, err ? err : "EINVAL:could not decode data"); + return; + } + + if (cmd->a == 'q') { + /* query only: validate and discard */ + free(pixels); + kitty_respond(cmd, 1, "OK"); + return; + } + + /* re-transmitting an existing id deletes the old image and placements */ + if (cmd->i && (im = kitty_find_id(cmd->i))) + kitty_delete_image(im); + + if (!(im = calloc(1, sizeof *im))) { + free(pixels); + kitty_respond(cmd, 0, "ENOMEM:out of memory"); + return; + } + im->id = cmd->i ? cmd->i : kitty_new_id(); + im->number = cmd->I; + im->width = w; + im->height = h; + im->pixels = pixels; + im->datasize = (size_t)w * h * 4; + im->transient = (cmd->N & 1) != 0; + im->current_frame = 1; + kitty_add_image(im); + kitty_ensure_rootframe(im); + + resp.i = im->id; + if (cmd->a == 'T') + kitty_do_display(cmd, im); + else + kitty_respond(&resp, 1, "OK"); +} + +/* work out the placement geometry and add it to the screen */ +static void +kitty_do_display(const KittyCmd *cmd, KittyImage *im) +{ + KittyPlacement *p, *old; + KittyCmd resp = *cmd; + int sx, sy, sw, sh; + int cols, rows; + int alt = IS_SET(MODE_ALTSCREEN) ? 1 : 0; + + if (!im) { + kitty_respond(cmd, 0, "ENOENT:image not found"); + return; + } + resp.i = im->id; + + sx = KMAX(cmd->x, 0); + sy = KMAX(cmd->y, 0); + sw = cmd->has_w && cmd->w > 0 ? cmd->w : im->width - sx; + sh = cmd->has_h && cmd->h > 0 ? cmd->h : im->height - sy; + sw = KMIN(sw, im->width - sx); + sh = KMIN(sh, im->height - sy); + if (sw <= 0 || sh <= 0) { + kitty_respond(&resp, 0, "EINVAL:source rectangle is empty"); + return; + } + + if (cmd->X >= win.cw || cmd->Y >= win.ch) { + kitty_respond(&resp, 0, "EINVAL:cell offset larger than cell size"); + return; + } + + /* size in cells */ + if (cmd->c > 0 && cmd->r > 0) { + cols = cmd->c; + rows = cmd->r; + } else if (cmd->c > 0) { + cols = cmd->c; + rows = KMAX(1, (int)((double)sh * (cols * win.cw) / ((double)sw * win.ch) + 0.5)); + } else if (cmd->r > 0) { + rows = cmd->r; + cols = KMAX(1, (int)((double)sw * (rows * win.ch) / ((double)sh * win.cw) + 0.5)); + } else { + cols = (sw + cmd->X + win.cw - 1) / win.cw; + rows = (sh + cmd->Y + win.ch - 1) / win.ch; + } + cols = KMAX(cols, 1); + rows = KMAX(rows, 1); + + if (cmd->U && (cmd->P || cmd->Q)) { + kitty_respond(&resp, 0, "EINVAL:virtual placements cannot be relative"); + return; + } + if (cmd->P) { + KittyImage *pim = kitty_find_id(cmd->P); + if (!pim || !kitty_find_placement(pim, cmd->Q)) { + kitty_respond(&resp, 0, "ENOPARENT:parent placement not found"); + return; + } + if (kitty_would_cycle(im->id, cmd->p, cmd->P, cmd->Q, 0)) { + kitty_respond(&resp, 0, "ECYCLE:relative placement would create a cycle"); + return; + } + } + + /* replace an existing placement with the same ids */ + if (im->id && cmd->p && (old = kitty_find_placement(im, cmd->p))) + kitty_delete_placement(old); + + if (!(p = calloc(1, sizeof *p))) { + kitty_respond(&resp, 0, "ENOMEM:out of memory"); + return; + } + p->image = im; + p->id = im->id ? cmd->p : 0; + p->virt = cmd->U ? 1 : 0; + p->sx = sx; + p->sy = sy; + p->sw = sw; + p->sh = sh; + p->xoff = KMAX(cmd->X, 0); + p->yoff = KMAX(cmd->Y, 0); + p->cols = cols; + p->rows = rows; + p->z = cmd->z; + p->alt = alt; + p->parent_img = cmd->P; + p->parent_plc = cmd->Q; + p->hoff = cmd->H; + p->voff = cmd->V; + p->pm_frame = -1; + p->x = term.c.x; + p->y = term.c.y + kitty_scr(); + kitty_add_placement(p); + + /* cursor movement: not for virtual or relative placements */ + if (!p->virt && !p->parent_img && cmd->C != 1) { + int nx = term.c.x + cols; + int ny = term.c.y + rows - 1; + if (ny > term.bot) + ny = term.bot; + term.c.x = KMIN(nx, term.col - 1); + term.c.y = ny; + term.c.state &= ~CURSOR_WRAPNEXT; + } + + tfulldirt(); + kitty_respond(&resp, 1, "OK"); +} + +/* a=d */ +static void +kitty_do_delete(const KittyCmd *cmd) +{ + KittyPlacement *p, *next; + KittyImage *im, *inext; + int freedata = (cmd->d >= 'A' && cmd->d <= 'Z'); + char what = (char)(freedata ? cmd->d + 32 : cmd->d); + int scr = kitty_scr(); + int s; + + /* an in-progress upload is aborted by a delete command */ + if (chunk.active) { + chunk.active = 0; + chunk.len = 0; + } + + for (s = 0; s < 2; s++) { + for (p = placements[s]; p; p = next) { + int px, py, del = 0; + next = p->next; + if (!kitty_resolve_pos(p, &px, &py, 0)) + px = p->x, py = p->y; + + switch (what) { + case 'a': + del = !p->virt && py - scr < term.row && py - scr + p->rows > 0; + break; + case 'i': + del = p->image && cmd->i && p->image->id == cmd->i && + (!cmd->has_p || p->id == cmd->p); + break; + case 'n': + { + KittyImage *nim = kitty_find_number(cmd->I); + del = nim && p->image == nim && + (!cmd->has_p || p->id == cmd->p); + break; + } + case 'c': + del = !p->virt && + term.c.x >= px && term.c.x < px + p->cols && + term.c.y + scr >= py && term.c.y + scr < py + p->rows; + break; + case 'p': + del = !p->virt && + cmd->x - 1 >= px && cmd->x - 1 < px + p->cols && + cmd->y - 1 + scr >= py && cmd->y - 1 + scr < py + p->rows; + break; + case 'q': + del = !p->virt && p->z == cmd->z && + cmd->x - 1 >= px && cmd->x - 1 < px + p->cols && + cmd->y - 1 + scr >= py && cmd->y - 1 + scr < py + p->rows; + break; + case 'x': + del = !p->virt && cmd->x - 1 >= px && cmd->x - 1 < px + p->cols; + break; + case 'y': + del = !p->virt && cmd->y - 1 + scr >= py && + cmd->y - 1 + scr < py + p->rows; + break; + case 'z': + del = !p->virt && p->z == cmd->z; + break; + case 'r': + del = p->image && p->image->id >= (uint32_t)cmd->x && + p->image->id <= (uint32_t)cmd->y; + break; + case 'f': + del = 0; /* handled below */ + break; + } + if (del) + kitty_delete_placement(p); + } + } + + if (what == 'f') { + im = cmd->i ? kitty_find_id(cmd->i) : kitty_find_number(cmd->I); + if (im) { + KittyFrame *root = im->frames; + if (root) { + KittyFrame *f, *fnext; + for (f = root->next; f; f = fnext) { + fnext = f->next; + if (f->pixels != im->pixels) + free(f->pixels); + free(f); + } + root->next = NULL; + } + im->nframes = im->frames ? 1 : 0; + im->current_frame = 1; + im->anim_state = 0; + } + tfulldirt(); + return; + } + + if (freedata) { + for (im = images; im; im = inext) { + inext = im->next; + switch (what) { + case 'i': + if (cmd->i && im->id == cmd->i && !im->refs) + kitty_delete_image(im); + break; + case 'n': + if (cmd->I && im->number == cmd->I && !im->refs) + kitty_delete_image(im); + break; + case 'r': + if (im->id >= (uint32_t)cmd->x && im->id <= (uint32_t)cmd->y && + !im->refs) + kitty_delete_image(im); + break; + default: + if (!im->refs) + kitty_delete_image(im); + break; + } + } + } + tfulldirt(); +} + +/* a=f : transmit animation frame data */ +static void +kitty_do_frame(const KittyCmd *cmd, const char *payload, size_t plen) +{ + KittyImage *im; + KittyFrame *f, *tail, *base = NULL, *edit = NULL; + unsigned char *data, *pixels; + size_t datalen = 0; + const char *err = NULL; + int w = 0, h = 0, fx, fy, xx, yy; + KittyCmd resp = *cmd; + + im = cmd->i ? kitty_find_id(cmd->i) : kitty_find_number(cmd->I); + if (!im) { + kitty_respond(cmd, 0, "ENOENT:image not found"); + return; + } + resp.i = im->id; + kitty_ensure_rootframe(im); + + data = kitty_read_medium(cmd, payload, plen, &datalen, &err); + if (!data) { + kitty_respond(&resp, 0, err ? err : "EINVAL:could not read data"); + return; + } + pixels = kitty_decode(cmd, data, datalen, &w, &h, &err); + free(data); + if (!pixels) { + kitty_respond(&resp, 0, err ? err : "EINVAL:could not decode data"); + return; + } + + fx = KMAX(cmd->x, 0); + fy = KMAX(cmd->y, 0); + if (fx + w > im->width || fy + h > im->height) { + free(pixels); + kitty_respond(&resp, 0, "EINVAL:frame rectangle out of bounds"); + return; + } + + if (cmd->has_c && cmd->c > 0) + base = kitty_get_frame(im, cmd->c); + if (cmd->has_r && cmd->r > 0) { + edit = kitty_get_frame(im, cmd->r); + if (!edit) { + free(pixels); + kitty_respond(&resp, 0, "ENOENT:frame not found"); + return; + } + } + + if (!edit) { + if (!(f = calloc(1, sizeof *f))) { + free(pixels); + kitty_respond(&resp, 0, "ENOMEM:out of memory"); + return; + } + f->width = im->width; + f->height = im->height; + f->gap = cmd->has_z ? cmd->z : 40; + f->transient = (cmd->N & 1) != 0; + if (!(f->pixels = malloc((size_t)im->width * im->height * 4))) { + free(f); + free(pixels); + kitty_respond(&resp, 0, "ENOMEM:out of memory"); + return; + } + if (base && base->pixels) { + memcpy(f->pixels, base->pixels, (size_t)im->width * im->height * 4); + } else { + /* fill with the background colour from the Y key */ + uint32_t bg = (uint32_t)cmd->Y; + unsigned char r = (bg >> 24) & 0xff, g = (bg >> 16) & 0xff; + unsigned char b = (bg >> 8) & 0xff, a = bg & 0xff; + size_t i, n = (size_t)im->width * im->height; + for (i = 0; i < n; i++) { + f->pixels[4*i+0] = r; + f->pixels[4*i+1] = g; + f->pixels[4*i+2] = b; + f->pixels[4*i+3] = a; + } + } + for (tail = im->frames; tail->next; tail = tail->next); + tail->next = f; + im->nframes++; + storage_used += (size_t)im->width * im->height * 4; + edit = f; + } else if (cmd->has_z) { + edit->gap = cmd->z; + } + + /* compose the transmitted rectangle onto the frame */ + for (yy = 0; yy < h; yy++) { + unsigned char *dst = edit->pixels + (((size_t)(fy+yy) * im->width) + fx) * 4; + const unsigned char *src = pixels + (size_t)yy * w * 4; + for (xx = 0; xx < w; xx++, dst += 4, src += 4) { + if (cmd->X == 1 || src[3] == 255) { + memcpy(dst, src, 4); + } else if (src[3]) { + int sa = src[3], da = dst[3]; + int oa = sa + da * (255 - sa) / 255; + int c; + if (oa <= 0) { + memset(dst, 0, 4); + } else { + for (c = 0; c < 3; c++) + dst[c] = (unsigned char) + ((src[c]*sa + dst[c]*da*(255-sa)/255) / oa); + dst[3] = (unsigned char)oa; + } + } + } + } + + free(pixels); + kitty_enforce_quota(); + tfulldirt(); + kitty_respond(&resp, 1, "OK"); +} + +/* a=a : animation control */ +static void +kitty_do_animctl(const KittyCmd *cmd) +{ + KittyImage *im; + KittyFrame *f; + KittyCmd resp = *cmd; + + im = cmd->i ? kitty_find_id(cmd->i) : kitty_find_number(cmd->I); + if (!im) { + kitty_respond(cmd, 0, "ENOENT:image not found"); + return; + } + resp.i = im->id; + kitty_ensure_rootframe(im); + + if (cmd->has_r && cmd->r > 0 && cmd->has_z) { + if (!(f = kitty_get_frame(im, cmd->r))) { + kitty_respond(&resp, 0, "ENOENT:frame not found"); + return; + } + f->gap = cmd->z; + } + if (cmd->has_c && cmd->c > 0) { + if (!kitty_get_frame(im, cmd->c)) { + kitty_respond(&resp, 0, "ENOENT:frame not found"); + return; + } + im->current_frame = cmd->c; + im->last_frame_time = kitty_now(); + tfulldirt(); + } + if (cmd->has_v && cmd->loops > 0) + im->loops = (cmd->loops == 1) ? -1 : cmd->loops - 1; + if (cmd->has_s && cmd->anim_state > 0) { + im->anim_state = cmd->anim_state; + if (cmd->anim_state == 1) + im->loops = 0; + im->last_frame_time = kitty_now(); + } + kitty_respond(&resp, 1, "OK"); +} + +/* a=c : compose animation frames */ +static void +kitty_do_compose(const KittyCmd *cmd) +{ + KittyImage *im; + KittyFrame *src, *dst; + int w, h, sx, sy, dx, dy, xx, yy; + KittyCmd resp = *cmd; + + im = cmd->i ? kitty_find_id(cmd->i) : kitty_find_number(cmd->I); + if (!im) { + kitty_respond(cmd, 0, "ENOENT:image not found"); + return; + } + resp.i = im->id; + kitty_ensure_rootframe(im); + + src = kitty_get_frame(im, cmd->r); + dst = kitty_get_frame(im, cmd->c); + if (!src || !dst || !src->pixels || !dst->pixels) { + kitty_respond(&resp, 0, "ENOENT:frame not found"); + return; + } + if (src == dst) { + kitty_respond(&resp, 0, "EINVAL:source and destination frames are the same"); + return; + } + + w = cmd->has_w && cmd->w > 0 ? cmd->w : im->width; + h = cmd->has_h && cmd->h > 0 ? cmd->h : im->height; + sx = KMAX(cmd->X, 0); + sy = KMAX(cmd->Y, 0); + dx = KMAX(cmd->x, 0); + dy = KMAX(cmd->y, 0); + if (sx + w > im->width || sy + h > im->height || + dx + w > im->width || dy + h > im->height) { + kitty_respond(&resp, 0, "EINVAL:rectangle out of bounds"); + return; + } + + for (yy = 0; yy < h; yy++) { + unsigned char *d = dst->pixels + (((size_t)(dy+yy) * im->width) + dx) * 4; + const unsigned char *s = src->pixels + (((size_t)(sy+yy) * im->width) + sx) * 4; + for (xx = 0; xx < w; xx++, d += 4, s += 4) { + if (cmd->C == 1 || s[3] == 255) { + memcpy(d, s, 4); + } else if (s[3]) { + int sa = s[3], da = d[3]; + int oa = sa + da * (255 - sa) / 255; + int c; + if (oa <= 0) { + memset(d, 0, 4); + } else { + for (c = 0; c < 3; c++) + d[c] = (unsigned char) + ((s[c]*sa + d[c]*da*(255-sa)/255) / oa); + d[3] = (unsigned char)oa; + } + } + } + } + tfulldirt(); + kitty_respond(&resp, 1, "OK"); +} + +/* dispatch a complete command */ +static void +kitty_exec(const KittyCmd *cmd, const char *payload, size_t plen) +{ + KittyImage *im; + + switch (cmd->a) { + case 't': + case 'T': + case 'q': + kitty_do_transmit(cmd, payload, plen); + break; + case 'p': + if (cmd->has_i && cmd->has_I && cmd->i && cmd->I) { + kitty_respond(cmd, 0, "EINVAL:both i and I specified"); + break; + } + im = cmd->i ? kitty_find_id(cmd->i) : kitty_find_number(cmd->I); + if (!im) + kitty_respond(cmd, 0, "ENOENT:image not found"); + else + kitty_do_display(cmd, im); + break; + case 'd': + kitty_do_delete(cmd); + break; + case 'f': + kitty_do_frame(cmd, payload, plen); + break; + case 'a': + kitty_do_animctl(cmd); + break; + case 'c': + kitty_do_compose(cmd); + break; + default: + kitty_respond(cmd, 0, "EINVAL:unknown action"); + break; + } +} + +/* ------------------------------------------------------------------ */ +/* APC entry point */ +/* ------------------------------------------------------------------ */ + +void +kitty_apc(const char *buf, size_t len) +{ + KittyCmd cmd; + const char *payload; + size_t plen, dlen; + char *decoded = NULL; + + if (len < 1 || buf[0] != 'G') + return; + buf++, len--; + + payload = kitty_parse(buf, len, &cmd); + plen = (size_t)((buf + len) - payload); + + /* decode the base64 payload */ + if (plen) { + if (!(decoded = malloc(plen + 4))) + return; + dlen = kitty_b64decode(payload, plen, decoded); + } else { + dlen = 0; + } + + if (chunk.active) { + /* continuation chunk */ + if (dlen) { + if (chunk.len + dlen > chunk.siz) { + size_t ncap = KMAX(chunk.siz * 2, chunk.len + dlen + 8192); + char *t = realloc(chunk.buf, ncap); + if (!t) { + chunk.active = 0; + chunk.len = 0; + free(decoded); + return; + } + chunk.buf = t; + chunk.siz = ncap; + } + memcpy(chunk.buf + chunk.len, decoded, dlen); + chunk.len += dlen; + } + free(decoded); + if (cmd.m == 0) { + KittyCmd done = chunk.cmd; + chunk.active = 0; + done.q = KMAX(done.q, cmd.q); + kitty_exec(&done, chunk.buf, chunk.len); + chunk.len = 0; + } + return; + } + + if (cmd.m == 1) { + /* first chunk of a chunked transfer */ + chunk.active = 1; + chunk.cmd = cmd; + chunk.len = 0; + if (dlen) { + if (dlen > chunk.siz) { + char *t = realloc(chunk.buf, dlen + 8192); + if (!t) { + chunk.active = 0; + free(decoded); + return; + } + chunk.buf = t; + chunk.siz = dlen + 8192; + } + memcpy(chunk.buf, decoded, dlen); + chunk.len = dlen; + } + free(decoded); + return; + } + + kitty_exec(&cmd, decoded ? decoded : "", dlen); + free(decoded); +} + +/* ------------------------------------------------------------------ */ +/* Unicode placeholders */ +/* ------------------------------------------------------------------ */ + +/* the row/column diacritics, derived from kitty's rowcolumn-diacritics.txt */ +static const uint32_t rowcolumn_diacritics[] = { + 0x0305,0x030D,0x030E,0x0310,0x0312,0x033D,0x033E,0x033F,0x0346,0x034A, + 0x034B,0x034C,0x0350,0x0351,0x0352,0x0357,0x035B,0x0363,0x0364,0x0365, + 0x0366,0x0367,0x0368,0x0369,0x036A,0x036B,0x036C,0x036D,0x036E,0x036F, + 0x0483,0x0484,0x0485,0x0486,0x0487,0x0592,0x0593,0x0594,0x0595,0x0597, + 0x0598,0x0599,0x059C,0x059D,0x059E,0x059F,0x05A0,0x05A1,0x05A8,0x05A9, + 0x05AB,0x05AC,0x05AF,0x05C4,0x0610,0x0611,0x0612,0x0613,0x0614,0x0615, + 0x0616,0x0617,0x0657,0x0658,0x0659,0x065A,0x065B,0x065D,0x065E,0x06D6, + 0x06D7,0x06D8,0x06D9,0x06DA,0x06DB,0x06DC,0x06DF,0x06E0,0x06E1,0x06E2, + 0x06E4,0x06E7,0x06E8,0x06EB,0x06EC,0x0730,0x0732,0x0733,0x0735,0x0736, + 0x073A,0x073D,0x073F,0x0740,0x0741,0x0743,0x0745,0x0747,0x0749,0x074A, + 0x07EB,0x07EC,0x07ED,0x07EE,0x07EF,0x07F0,0x07F1,0x07F3,0x0816,0x0817, + 0x0818,0x0819,0x081B,0x081C,0x081D,0x081E,0x081F,0x0820,0x0821,0x0822, + 0x0823,0x0825,0x0826,0x0827,0x0829,0x082A,0x082B,0x082C,0x082D,0x0951, + 0x0953,0x0954,0x0F82,0x0F83,0x0F86,0x0F87,0x135D,0x135E,0x135F,0x17DD, + 0x193A,0x1A17,0x1A75,0x1A76,0x1A77,0x1A78,0x1A79,0x1A7A,0x1A7B,0x1A7C, + 0x1B6B,0x1B6D,0x1B6E,0x1B6F,0x1B70,0x1B71,0x1B72,0x1B73,0x1CD0,0x1CD1, + 0x1CD2,0x1CDA,0x1CDB,0x1CE0,0x1DC0,0x1DC1,0x1DC3,0x1DC4,0x1DC5,0x1DC6, + 0x1DC7,0x1DC8,0x1DC9,0x1DCB,0x1DCC,0x1DD1,0x1DD2,0x1DD3,0x1DD4,0x1DD5, + 0x1DD6,0x1DD7,0x1DD8,0x1DD9,0x1DDA,0x1DDB,0x1DDC,0x1DDD,0x1DDE,0x1DDF, + 0x1DE0,0x1DE1,0x1DE2,0x1DE3,0x1DE4,0x1DE5,0x1DE6,0x1DFE,0x20D0,0x20D1, + 0x20D4,0x20D5,0x20D6,0x20D7,0x20DB,0x20DC,0x20E1,0x20E7,0x20E9,0x20F0, + 0x2CEF,0x2CF0,0x2CF1,0x2DE0,0x2DE1,0x2DE2,0x2DE3,0x2DE4,0x2DE5,0x2DE6, + 0x2DE7,0x2DE8,0x2DE9,0x2DEA,0x2DEB,0x2DEC,0x2DED,0x2DEE,0x2DEF,0x2DF0, + 0x2DF1,0x2DF2,0x2DF3,0x2DF4,0x2DF5,0x2DF6,0x2DF7,0x2DF8,0x2DF9,0x2DFA, + 0x2DFB,0x2DFC,0x2DFD,0x2DFE,0x2DFF,0xA66F,0xA67C,0xA67D,0xA6F0,0xA6F1, + 0xA8E0,0xA8E1,0xA8E2,0xA8E3,0xA8E4,0xA8E5,0xA8E6,0xA8E7,0xA8E8,0xA8E9, + 0xA8EA,0xA8EB,0xA8EC,0xA8ED,0xA8EE,0xA8EF,0xA8F0,0xA8F1,0xAAB0,0xAAB2, + 0xAAB3,0xAAB7,0xAAB8,0xAABE,0xAABF,0xAAC1,0xFE20,0xFE21,0xFE22,0xFE23, + 0xFE24,0xFE25,0xFE26,0x10A0F,0x10A38,0x1D185,0x1D186,0x1D187,0x1D188, + 0x1D189,0x1D1AA,0x1D1AB,0x1D1AC,0x1D1AD,0x1D242,0x1D243,0x1D244, +}; +#define NDIACRITICS (sizeof rowcolumn_diacritics / sizeof *rowcolumn_diacritics) + +static int +kitty_diacritic_index(Rune u) +{ + size_t lo = 0, hi = NDIACRITICS; + + while (lo < hi) { + size_t mid = (lo + hi) / 2; + if (rowcolumn_diacritics[mid] == u) + return (int)mid; + if (rowcolumn_diacritics[mid] < u) + lo = mid + 1; + else + hi = mid; + } + return -1; +} + +/* + * Placeholder cells are marked with ATTR_KITTYPH and carry their row, + * column and image-id most-significant-byte in the glyph itself, so that + * they scroll and reflow along with the text like any other character. + */ +int +kitty_is_diacritic(Rune u) +{ + return kitty_diacritic_index(u) >= 0; +} + +/* absorb a combining diacritic into the placeholder glyph gp */ +int +kitty_absorb_diacritic(Glyph *gp, Rune u) +{ + int idx = kitty_diacritic_index(u); + uint32_t k; + + if (idx < 0 || !(gp->mode & ATTR_KITTYPH)) + return 0; + k = gp->kitty; + if (!(k & KPH_HAS_ROW)) { + k |= KPH_HAS_ROW | ((uint32_t)(idx & 0xff) << KPH_ROW_SHIFT); + } else if (!(k & KPH_HAS_COL)) { + k |= KPH_HAS_COL | ((uint32_t)(idx & 0xff) << KPH_COL_SHIFT); + } else if (!(k & KPH_HAS_MSB)) { + k |= KPH_HAS_MSB | ((uint32_t)(idx & 0xff) << KPH_MSB_SHIFT); + } else { + return 0; + } + gp->kitty = k; + return 1; +} + +/* ------------------------------------------------------------------ */ +/* rendering */ +/* ------------------------------------------------------------------ */ + +/* nearest-neighbour + bilinear scaling of a source rect to w x h RGBA */ +static unsigned char * +kitty_scale(const unsigned char *src, int sw, int sh, int stride, + int dw, int dh) +{ + unsigned char *dst; + int x, y, c; + + if (dw <= 0 || dh <= 0) + return NULL; + if (!(dst = malloc((size_t)dw * dh * 4))) + return NULL; + + for (y = 0; y < dh; y++) { + double fy = ((double)y + 0.5) * sh / dh - 0.5; + int y0 = (int)(fy < 0 ? 0 : fy); + int y1 = KMIN(y0 + 1, sh - 1); + int wy = (int)((fy - y0) * 256); + if (wy < 0) wy = 0; + if (wy > 256) wy = 256; + for (x = 0; x < dw; x++) { + double fx = ((double)x + 0.5) * sw / dw - 0.5; + int x0 = (int)(fx < 0 ? 0 : fx); + int x1 = KMIN(x0 + 1, sw - 1); + int wx = (int)((fx - x0) * 256); + const unsigned char *p00, *p01, *p10, *p11; + unsigned char *o = dst + ((size_t)y * dw + x) * 4; + if (wx < 0) wx = 0; + if (wx > 256) wx = 256; + p00 = src + (size_t)y0 * stride + x0 * 4; + p01 = src + (size_t)y0 * stride + x1 * 4; + p10 = src + (size_t)y1 * stride + x0 * 4; + p11 = src + (size_t)y1 * stride + x1 * 4; + for (c = 0; c < 4; c++) { + int top = p00[c] * (256 - wx) + p01[c] * wx; + int bot = p10[c] * (256 - wx) + p11[c] * wx; + o[c] = (unsigned char)((top * (256 - wy) + bot * wy) >> 16); + } + } + } + return dst; +} + +/* build (or reuse) the scaled ARGB pixmap for a placement */ +static int +kitty_prepare(KittyPlacement *p, int *outw, int *outh) +{ + KittyImage *im = p->image; + const unsigned char *base; + unsigned char *scaled = NULL, *bgra; + int w = p->cols * win.cw - p->xoff; + int h = p->rows * win.ch - p->yoff; + int i, n; + XImage ximage; + + if (!im || w <= 0 || h <= 0) + return 0; + + if (p->pixmap && p->pm_w == w && p->pm_h == h && + p->pm_frame == im->current_frame) { + *outw = w; + *outh = h; + return 1; + } + kitty_freepixmap(p); + + base = kitty_frame_pixels(im, im->current_frame); + if (!base) + return 0; + base += ((size_t)p->sy * im->width + p->sx) * 4; + + scaled = kitty_scale(base, p->sw, p->sh, im->width * 4, w, h); + if (!scaled) + return 0; + + /* premultiply and convert RGBA -> BGRA for XPutImage on a 32-bit visual */ + bgra = scaled; + n = w * h; + for (i = 0; i < n; i++) { + unsigned char r = bgra[4*i+0], g = bgra[4*i+1]; + unsigned char b = bgra[4*i+2], a = bgra[4*i+3]; + bgra[4*i+0] = (unsigned char)(b * a / 255); + bgra[4*i+1] = (unsigned char)(g * a / 255); + bgra[4*i+2] = (unsigned char)(r * a / 255); + bgra[4*i+3] = a; + } + + p->pixmap = (void *)(uintptr_t)XCreatePixmap(xw.dpy, xw.win, w, h, 32); + if (!p->pixmap) { + free(scaled); + return 0; + } + + memset(&ximage, 0, sizeof ximage); + ximage.format = ZPixmap; + ximage.data = (char *)bgra; + ximage.width = w; + ximage.height = h; + ximage.byte_order = LSBFirst; + ximage.bitmap_bit_order = MSBFirst; + ximage.bits_per_pixel = 32; + ximage.bytes_per_line = w * 4; + ximage.bitmap_unit = 32; + ximage.bitmap_pad = 32; + ximage.depth = 32; + XInitImage(&ximage); + + { + GC gc = XCreateGC(xw.dpy, (Drawable)p->pixmap, 0, NULL); + XPutImage(xw.dpy, (Drawable)p->pixmap, gc, &ximage, 0, 0, 0, 0, w, h); + XFreeGC(xw.dpy, gc); + } + free(scaled); + + p->pm_w = w; + p->pm_h = h; + p->pm_frame = im->current_frame; + *outw = w; + *outh = h; + return 1; +} + +static void +kitty_blit(KittyPlacement *p, int px, int py) +{ + Picture src, dstpic; + XRenderPictFormat *fmt; + XRenderPictureAttributes pa; + int w, h, destx, desty, clipw, cliph; + int bx, by; + + if (!kitty_prepare(p, &w, &h)) + return; + + kitty_border(&bx, &by); + + destx = bx + px * win.cw + p->xoff; + desty = by + (py - kitty_scr()) * win.ch + p->yoff; + + /* clip to the terminal area */ + clipw = KMIN(w, bx + win.tw - destx); + cliph = KMIN(h, by + win.th - desty); + if (clipw <= 0 || cliph <= 0) + return; + + memset(&pa, 0, sizeof pa); + fmt = XRenderFindStandardFormat(xw.dpy, PictStandardARGB32); + if (!fmt) + return; + src = XRenderCreatePicture(xw.dpy, (Drawable)p->pixmap, fmt, 0, &pa); + fmt = XRenderFindVisualFormat(xw.dpy, xw.vis); + if (!fmt) { + XRenderFreePicture(xw.dpy, src); + return; + } + dstpic = XRenderCreatePicture(xw.dpy, xw.buf, fmt, 0, &pa); + XRenderComposite(xw.dpy, PictOpOver, src, None, dstpic, + 0, 0, 0, 0, destx, desty, clipw, cliph); + XRenderFreePicture(xw.dpy, src); + XRenderFreePicture(xw.dpy, dstpic); +} + +/* comparison for the drawing order: z-index, then image id */ +static int +kitty_cmp(const void *a, const void *b) +{ + KittyPlacement * const *pa = a, * const *pb = b; + uint32_t ia, ib; + + if ((*pa)->z != (*pb)->z) + return (*pa)->z < (*pb)->z ? -1 : 1; + ia = (*pa)->image ? (*pa)->image->id : 0; + ib = (*pb)->image ? (*pb)->image->id : 0; + if (ia != ib) + return ia < ib ? -1 : 1; + return 0; +} + +/* advance animations, returns 1 if any animation is running */ +static int +kitty_step_animations(uint64_t now, uint64_t *nextwake) +{ + KittyImage *im; + int running = 0; + + for (im = images; im; im = im->next) { + KittyFrame *f; + int guard; + + if (im->anim_state < 2 || im->nframes < 2 || !im->refs) + continue; + if (!im->last_frame_time) + im->last_frame_time = now; + + for (guard = 0; guard < im->nframes + 1; guard++) { + int gap; + f = kitty_get_frame(im, im->current_frame); + gap = f ? f->gap : 40; + if (gap < 0) + gap = 0; /* gapless: skip immediately */ + if (now < im->last_frame_time + (uint64_t)gap) { + uint64_t due = im->last_frame_time + gap - now; + if (!*nextwake || due < *nextwake) + *nextwake = due; + break; + } + im->last_frame_time += gap; + if (im->current_frame >= im->nframes) { + if (im->anim_state == 2) + break; /* loading mode: wait for more frames */ + if (im->loops > 0) + im->loops--; + else if (im->loops == 0 && im->anim_state == 3 && 0) + break; + im->current_frame = 1; + } else { + im->current_frame++; + } + tfulldirt(); + } + running = 1; + } + return running; +} + +int +kitty_animating(double *timeout_ms) +{ + uint64_t next = 0; + int running = kitty_step_animations(kitty_now(), &next); + + if (running && timeout_ms) + *timeout_ms = next ? (double)next : 1.0; + return running; +} + +/* find the virtual placement of an image, preferring the given placement id */ +static KittyPlacement * +kitty_find_virtual(uint32_t imgid, uint32_t plcid) +{ + KittyImage *im = kitty_find_id(imgid); + KittyPlacement *p, *any = NULL; + int s; + + if (!im) + return NULL; + for (s = 0; s < 2; s++) + for (p = placements[s]; p; p = p->next) { + if (p->image != im || !p->virt) + continue; + if (plcid && p->id == plcid) + return p; + if (!any) + any = p; + } + return plcid ? NULL : any; +} + +/* + * Blit the part of a virtual placement that corresponds to one screen cell. + * (px, py) is the cell on screen, (row, col) the cell within the placement. + */ +static void +kitty_blit_cell(KittyPlacement *p, int px, int py, int row, int col) +{ + KittyImage *im = p->image; + const unsigned char *base; + unsigned char *cell; + int cw = win.cw, ch = win.ch; + int fullw, fullh, sx0, sy0, x, y, i, n; + unsigned char *tile; + Pixmap pm; + XImage ximage; + Picture src, dstpic; + XRenderPictFormat *fmt; + XRenderPictureAttributes pa; + int bx, by; + + if (!im || row >= p->rows || col >= p->cols) + return; + + fullw = p->cols * cw; + fullh = p->rows * ch; + if (fullw <= 0 || fullh <= 0) + return; + + base = kitty_frame_pixels(im, im->current_frame); + if (!base) + return; + base += ((size_t)p->sy * im->width + p->sx) * 4; + + /* scale just this cell out of the source rect */ + sx0 = col * cw; + sy0 = row * ch; + if (!(cell = malloc((size_t)cw * ch * 4))) + return; + for (y = 0; y < ch; y++) { + double fy = ((double)(sy0 + y) + 0.5) * p->sh / fullh - 0.5; + int y0 = (int)(fy < 0 ? 0 : fy); + int y1 = KMIN(y0 + 1, p->sh - 1); + int wy = (int)((fy - y0) * 256); + if (wy < 0) wy = 0; + if (wy > 256) wy = 256; + for (x = 0; x < cw; x++) { + double fx = ((double)(sx0 + x) + 0.5) * p->sw / fullw - 0.5; + int x0 = (int)(fx < 0 ? 0 : fx); + int x1 = KMIN(x0 + 1, p->sw - 1); + int wx = (int)((fx - x0) * 256); + const unsigned char *q00, *q01, *q10, *q11; + unsigned char *o = cell + ((size_t)y * cw + x) * 4; + int c; + if (wx < 0) wx = 0; + if (wx > 256) wx = 256; + q00 = base + (size_t)y0 * im->width * 4 + x0 * 4; + q01 = base + (size_t)y0 * im->width * 4 + x1 * 4; + q10 = base + (size_t)y1 * im->width * 4 + x0 * 4; + q11 = base + (size_t)y1 * im->width * 4 + x1 * 4; + for (c = 0; c < 4; c++) { + int t = q00[c] * (256 - wx) + q01[c] * wx; + int b = q10[c] * (256 - wx) + q11[c] * wx; + o[c] = (unsigned char)((t * (256 - wy) + b * wy) >> 16); + } + } + } + + tile = cell; + n = cw * ch; + for (i = 0; i < n; i++) { + unsigned char r = tile[4*i+0], g = tile[4*i+1]; + unsigned char b = tile[4*i+2], a = tile[4*i+3]; + tile[4*i+0] = (unsigned char)(b * a / 255); + tile[4*i+1] = (unsigned char)(g * a / 255); + tile[4*i+2] = (unsigned char)(r * a / 255); + tile[4*i+3] = a; + } + + pm = XCreatePixmap(xw.dpy, xw.win, cw, ch, 32); + if (!pm) { + free(cell); + return; + } + memset(&ximage, 0, sizeof ximage); + ximage.format = ZPixmap; + ximage.data = (char *)tile; + ximage.width = cw; + ximage.height = ch; + ximage.byte_order = LSBFirst; + ximage.bitmap_bit_order = MSBFirst; + ximage.bits_per_pixel = 32; + ximage.bytes_per_line = cw * 4; + ximage.bitmap_unit = 32; + ximage.bitmap_pad = 32; + ximage.depth = 32; + XInitImage(&ximage); + { + GC gc = XCreateGC(xw.dpy, pm, 0, NULL); + XPutImage(xw.dpy, pm, gc, &ximage, 0, 0, 0, 0, cw, ch); + XFreeGC(xw.dpy, gc); + } + free(cell); + + kitty_border(&bx, &by); + + memset(&pa, 0, sizeof pa); + fmt = XRenderFindStandardFormat(xw.dpy, PictStandardARGB32); + if (fmt) { + src = XRenderCreatePicture(xw.dpy, pm, fmt, 0, &pa); + fmt = XRenderFindVisualFormat(xw.dpy, xw.vis); + if (fmt) { + dstpic = XRenderCreatePicture(xw.dpy, xw.buf, fmt, 0, &pa); + XRenderComposite(xw.dpy, PictOpOver, src, None, dstpic, 0, 0, 0, 0, + bx + px * cw, by + py * ch, cw, ch); + XRenderFreePicture(xw.dpy, dstpic); + } + XRenderFreePicture(xw.dpy, src); + } + XFreePixmap(xw.dpy, pm); +} + +/* scan the visible screen for placeholder glyphs and draw their images */ +static void +kitty_draw_placeholders(void) +{ + int x, y; + uint32_t prev_fg = 0, prev_ul = 0, prev_msb = 0; + int prev_row = -1, prev_col = -1, prev_valid = 0, prev_x = -2; + + for (y = 0; y < term.row; y++) { + #if REFLOW_PATCH || SCROLLBACK_PATCH + Line line = TLINE(y); + #else + Line line = term.line[y]; + #endif + prev_valid = 0; + prev_x = -2; + for (x = 0; x < term.col; x++) { + Glyph *gp = &line[x]; + uint32_t k, fg, ul, imgid, plcid; + int row, col, msb; + KittyPlacement *vp; + + if (!(gp->mode & ATTR_KITTYPH)) { + prev_valid = 0; + continue; + } + k = gp->kitty; + fg = gp->fg; + #if UNDERCURL_PATCH + ul = (uint32_t)((gp->ucolor[0] << 16) | (gp->ucolor[1] << 8) | + gp->ucolor[2]); + if (gp->ucolor[0] < 0) + ul = 0; + #else + ul = 0; + #endif + + row = (k & KPH_HAS_ROW) ? (int)((k >> KPH_ROW_SHIFT) & 0xff) : -1; + col = (k & KPH_HAS_COL) ? (int)((k >> KPH_COL_SHIFT) & 0xff) : -1; + msb = (k & KPH_HAS_MSB) ? (int)((k >> KPH_MSB_SHIFT) & 0xff) : -1; + + /* inherit from the cell to the left where allowed */ + if (prev_valid && prev_x == x - 1 && fg == prev_fg && ul == prev_ul) { + if (row < 0) { + row = prev_row; + if (col < 0) + col = prev_col + 1; + } + if (msb < 0) + msb = (int)prev_msb; + } + if (row < 0) + row = 0; + if (col < 0) + col = 0; + if (msb < 0) + msb = 0; + + /* image id: foreground colour plus the msb diacritic */ + if (IS_TRUECOL(fg)) + imgid = fg & 0xffffff; + else + imgid = fg & 0xff; + imgid |= (uint32_t)msb << 24; + plcid = ul; + + vp = kitty_find_virtual(imgid, plcid); + if (vp) + kitty_blit_cell(vp, x, y, row, col); + + prev_valid = 1; + prev_x = x; + prev_fg = fg; + prev_ul = ul; + prev_row = row; + prev_col = col; + prev_msb = (uint32_t)msb; + } + } +} + +void +kitty_draw(void) +{ + KittyPlacement **list = NULL, *p; + int n = 0, cap = 0, i; + int s = IS_SET(MODE_ALTSCREEN) ? 1 : 0; + int scr = kitty_scr(); + uint64_t next = 0; + + kitty_step_animations(kitty_now(), &next); + + for (p = placements[s]; p; p = p->next) { + int px, py; + if (p->virt) + continue; + if (!kitty_resolve_pos(p, &px, &py, 0)) + continue; + if (px >= term.col || py - scr >= term.row || py - scr + p->rows <= 0) + continue; + if (n >= cap) { + int ncap = cap ? cap * 2 : 32; + KittyPlacement **t = realloc(list, ncap * sizeof *t); + if (!t) + break; + list = t; + cap = ncap; + } + list[n++] = p; + } + + if (n > 1) + qsort(list, n, sizeof *list, kitty_cmp); + + for (i = 0; i < n; i++) { + int px, py; + if (kitty_resolve_pos(list[i], &px, &py, 0)) + kitty_blit(list[i], px, py); + } + + free(list); + + /* images displayed via Unicode placeholders */ + kitty_draw_placeholders(); +} + +/* ------------------------------------------------------------------ */ +/* terminal lifecycle hooks */ +/* ------------------------------------------------------------------ */ + +void +kitty_scroll(int top, int bot, int n, int save) +{ + KittyPlacement *p, *next; + int alt = IS_SET(MODE_ALTSCREEN) ? 1 : 0; + int scr = kitty_scr(); + int itop = top + scr, ibot = bot + scr; + + for (p = placements[alt]; p; p = next) { + next = p->next; + if (alt || !save) { + if (p->y >= itop && p->y <= ibot) { + p->y -= n; + if (p->y + p->rows <= itop) + kitty_delete_placement(p); + } + } else { + p->y -= scr; + if (p->y < 0) { + p->y -= n; + } else if (p->y >= top && p->y <= bot) { + p->y -= n; + } + if (p->y + p->rows <= -HISTSIZE) + kitty_delete_placement(p); + else + p->y += term.scr; + } + } +} + +/* used by kscrollup/kscrolldown: shift everything by n */ +void +kitty_trimhistory(int n) +{ + KittyPlacement *p, *next; + int alt = IS_SET(MODE_ALTSCREEN) ? 1 : 0; + + for (p = placements[alt]; p; p = next) { + next = p->next; + p->y += n; + if (p->y + p->rows <= -HISTSIZE) + kitty_delete_placement(p); + } +} + +void +kitty_deleteplacements(int alt) +{ + KittyPlacement *p, *next; + + for (p = placements[alt ? 1 : 0]; p; p = next) { + next = p->next; + kitty_delete_placement(p); + } +} + +void +kitty_clearregion(int x1, int y1, int x2, int y2) +{ + KittyPlacement *p, *next; + int alt = IS_SET(MODE_ALTSCREEN) ? 1 : 0; + int scr = kitty_scr(); + + for (p = placements[alt]; p; p = next) { + int px, py; + next = p->next; + if (!kitty_resolve_pos(p, &px, &py, 0)) + continue; + py -= scr; + if (px < x1 || px > x2 || py + p->rows - 1 < y1 || py > y2) + continue; + kitty_delete_placement(p); + } +} + +void +kitty_reset(void) +{ + KittyImage *im, *next; + + kitty_deleteplacements(0); + kitty_deleteplacements(1); + for (im = images; im; im = next) { + next = im->next; + kitty_delete_image(im); + } + images = NULL; + storage_used = 0; + chunk.active = 0; + chunk.len = 0; +} + +void +kitty_altscreen(int alt) +{ + /* placements are kept per screen; nothing to do but redraw */ + (void)alt; + tfulldirt(); +} + +void +kitty_resize(void) +{ + KittyPlacement *p; + int s; + + /* cell size may have changed, so the cached pixmaps are stale */ + for (s = 0; s < 2; s++) + for (p = placements[s]; p; p = p->next) + kitty_freepixmap(p); +} + +#endif // KITTY_GRAPHICS_PATCH diff --git a/kitty.h b/kitty.h new file mode 100644 index 0000000..8e1cd0c --- /dev/null +++ b/kitty.h @@ -0,0 +1,91 @@ +/* kitty.h - kitty terminal graphics protocol for st + * + * See https://sw.kovidgoyal.net/kitty/graphics-protocol/ + */ +#ifndef KITTY_H +#define KITTY_H + +#if KITTY_GRAPHICS_PATCH + +#include + +/* the Unicode placeholder character */ +#define KITTY_PLACEHOLDER 0x10EEEE + +/* placeholder glyph bits packed into Glyph.kitty */ +#define KPH_HAS_ROW (1u << 0) +#define KPH_HAS_COL (1u << 1) +#define KPH_HAS_MSB (1u << 2) +#define KPH_ROW_SHIFT 8 +#define KPH_COL_SHIFT 16 +#define KPH_MSB_SHIFT 24 + +/* limits */ +#define KITTY_MAX_DIM 16384 +#define KITTY_STORAGE_QUOTA (320u * 1024u * 1024u) +#define KITTY_MAX_REL_DEPTH 8 + +/* a decoded RGBA image (or animation frame) */ +typedef struct KittyFrame { + struct KittyFrame *next; + unsigned char *pixels; /* RGBA, width*height*4, may be NULL for lazy frames */ + int width, height; + int gap; /* ms until next frame; <0 == gapless */ + int transient; +} KittyFrame; + +typedef struct KittyImage { + struct KittyImage *next, *prev; + uint32_t id; /* image id (i key) */ + uint32_t number; /* image number (I key) */ + int width, height; /* dimensions of the root frame */ + unsigned char *pixels; /* root frame RGBA data */ + size_t datasize; + KittyFrame *frames; /* animation frames, frame 1 == root (pixels) */ + int nframes; + int current_frame; /* 1-based */ + int anim_state; /* 0 stopped, 2 loading, 3 running */ + int loops; /* remaining loops, -1 infinite */ + int transient; + uint64_t last_frame_time;/* ms of last frame advance */ + unsigned refs; /* number of placements */ +} KittyImage; + +typedef struct KittyPlacement { + struct KittyPlacement *next, *prev; + KittyImage *image; + uint32_t id; /* placement id (p key) */ + int virt; /* U=1 virtual placement (Unicode placeholder) */ + /* source rect in image pixels */ + int sx, sy, sw, sh; + /* screen position */ + int x, y; /* cell coordinates; y is in scrollback space */ + int xoff, yoff; /* pixel offset within the first cell */ + int cols, rows; /* size on screen in cells */ + int z; + int alt; /* belongs to the alternate screen */ + /* relative placement */ + uint32_t parent_img, parent_plc; + int hoff, voff; + /* scaled pixmap cache */ + void *pixmap; + int pm_w, pm_h; /* pixel size the pixmap was rendered at */ + int pm_frame; /* frame the pixmap was rendered from */ +} KittyPlacement; + +/* entry points used by st.c / x.c */ +void kitty_apc(const char *buf, size_t len); +void kitty_draw(void); +void kitty_scroll(int top, int bot, int n, int save); +void kitty_deleteplacements(int alt); +void kitty_clearregion(int x1, int y1, int x2, int y2); +void kitty_reset(void); +void kitty_altscreen(int alt); +void kitty_resize(void); +void kitty_trimhistory(int histsize); +int kitty_animating(double *timeout_ms); +int kitty_is_diacritic(Rune u); +int kitty_absorb_diacritic(Glyph *gp, Rune u); + +#endif // KITTY_GRAPHICS_PATCH +#endif // KITTY_H diff --git a/patch/reflow.c b/patch/reflow.c index 2fd80f0..c95bc82 100644 --- a/patch/reflow.c +++ b/patch/reflow.c @@ -9,6 +9,9 @@ tloaddefscreen(int clear, int loadcursor) #if SIXEL_PATCH tdeleteimages(); #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_deleteplacements(1); + #endif // KITTY_GRAPHICS_PATCH } col = term.col, row = term.row; tswapscreen(); @@ -37,6 +40,9 @@ tloadaltscreen(int clear, int savecursor) #if SIXEL_PATCH tdeleteimages(); #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_deleteplacements(1); + #endif // KITTY_GRAPHICS_PATCH } } @@ -59,6 +65,9 @@ tclearglyph(Glyph *gp, int usecurattr) } gp->mode = ATTR_NULL; gp->u = ' '; + #if KITTY_GRAPHICS_PATCH + gp->kitty = 0; + #endif // KITTY_GRAPHICS_PATCH } #if SIXEL_PATCH @@ -268,6 +277,9 @@ rscrolldown(int n) #if SIXEL_PATCH scroll_images(n - term.scr); #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_trimhistory(n - term.scr); + #endif // KITTY_GRAPHICS_PATCH term.scr = 0; if (sel.ob.x != -1 && !sel.alt) selmove(-i); @@ -340,6 +352,9 @@ tresizealt(int col, int row) #if SIXEL_PATCH scroll_images(-i); #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_trimhistory(-i); + #endif // KITTY_GRAPHICS_PATCH term.c.y = row - 1; } for (i += row; i < term.row; i++) @@ -412,6 +427,9 @@ kscrolldown(const Arg* a) #if SIXEL_PATCH scroll_images(-1*n); #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_trimhistory(-1*n); + #endif // KITTY_GRAPHICS_PATCH #if OPENURLONCLICK_PATCH if (n > 0) @@ -444,6 +462,9 @@ kscrollup(const Arg* a) #if SIXEL_PATCH scroll_images(n); #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_trimhistory(n); + #endif // KITTY_GRAPHICS_PATCH #if OPENURLONCLICK_PATCH if (n > 0) @@ -501,6 +522,10 @@ tscrollup(int top, int bot, int n, int mode) term.line[i+n] = temp; } + #if KITTY_GRAPHICS_PATCH + kitty_scroll(top, bot, n, savehist); + #endif // KITTY_GRAPHICS_PATCH + #if SIXEL_PATCH if (alt || !savehist) { /* move images, if they are inside the scrolling region */ @@ -571,6 +596,10 @@ tscrolldown(int top, int n) term.line[i-n] = temp; } + #if KITTY_GRAPHICS_PATCH + kitty_scroll(top, bot, -n, 0); + #endif // KITTY_GRAPHICS_PATCH + #if SIXEL_PATCH /* move images, if they are inside the scrolling region */ for (im = term.images; im; im = next) { diff --git a/patches.def.h b/patches.def.h index a27800c..1e4edd6 100644 --- a/patches.def.h +++ b/patches.def.h @@ -343,6 +343,20 @@ */ #define SINGLE_DRAWABLE_BUFFER_PATCH 0 +/* This patch adds support for the kitty terminal graphics protocol, which lets + * clients display raster images in the terminal. It supports the RGB, RGBA and + * PNG formats, zlib compression, direct/file/temp-file/shared-memory + * transmission, placements (including relative and Unicode-placeholder based + * virtual placements), z-index ordering with alpha blending, image deletion, + * queries and animations. + * + * Note that you need to uncomment the corresponding lines in config.mk when + * including this patch (it needs zlib and Xrender). + * + * https://sw.kovidgoyal.net/kitty/graphics-protocol/ + */ +#define KITTY_GRAPHICS_PATCH 0 + /* This patch adds SIXEL graphics support for st. * Note that patch/sixel.c/sixel_hls.c come from mintty, licensed under GPL. * Known issues: diff --git a/patches.h b/patches.h index c8bd7f8..135f733 100644 --- a/patches.h +++ b/patches.h @@ -343,6 +343,20 @@ */ #define SINGLE_DRAWABLE_BUFFER_PATCH 0 +/* This patch adds support for the kitty terminal graphics protocol, which lets + * clients display raster images in the terminal. It supports the RGB, RGBA and + * PNG formats, zlib compression, direct/file/temp-file/shared-memory + * transmission, placements (including relative and Unicode-placeholder based + * virtual placements), z-index ordering with alpha blending, image deletion, + * queries and animations. + * + * Note that you need to uncomment the corresponding lines in config.mk when + * including this patch (it needs zlib and Xrender). + * + * https://sw.kovidgoyal.net/kitty/graphics-protocol/ + */ +#define KITTY_GRAPHICS_PATCH 1 + /* This patch adds SIXEL graphics support for st. * Note that patch/sixel.c/sixel_hls.c come from mintty, licensed under GPL. * Known issues: diff --git a/st.c b/st.c index e643239..b3fbf25 100644 --- a/st.c +++ b/st.c @@ -30,6 +30,10 @@ #include "sixel.h" #endif // SIXEL_PATCH +#if KITTY_GRAPHICS_PATCH +#include "kitty.h" +#endif // KITTY_GRAPHICS_PATCH + #if defined(__linux) #include #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) @@ -1227,6 +1231,9 @@ treset(void) #if SIXEL_PATCH tdeleteimages(); #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_deleteplacements(i); + #endif // KITTY_GRAPHICS_PATCH tswapscreen(); } #if REFLOW_PATCH @@ -1595,6 +1602,14 @@ tsetchar(Rune u, const Glyph *attr, int x, int y) if (isboxdraw(u)) term.line[y][x].mode |= ATTR_BOXDRAW; #endif // BOXDRAW_PATCH + + #if KITTY_GRAPHICS_PATCH + term.line[y][x].kitty = 0; + if (u == KITTY_PLACEHOLDER) + term.line[y][x].mode |= ATTR_KITTYPH; + else + term.line[y][x].mode &= ~ATTR_KITTYPH; + #endif // KITTY_GRAPHICS_PATCH } #if !REFLOW_PATCH @@ -2219,6 +2234,10 @@ csihandle(void) #endif // REFLOW_PATCH break; case 2: /* screen */ + #if KITTY_GRAPHICS_PATCH + /* the clear screen escape also clears all images */ + kitty_deleteplacements(IS_SET(MODE_ALTSCREEN)); + #endif // KITTY_GRAPHICS_PATCH #if REFLOW_PATCH if (IS_SET(MODE_ALTSCREEN)) { tclearregion(0, 0, term.col-1, term.row-1, 1); @@ -2603,6 +2622,16 @@ strhandle(void) #endif // SIXEL_PATCH term.esc &= ~(ESC_STR_END|ESC_STR); + + #if KITTY_GRAPHICS_PATCH + /* handled before strparse(), which destroys the ';' separators */ + if (strescseq.type == '_') { + strescseq.buf[strescseq.len] = '\0'; + kitty_apc(strescseq.buf, strescseq.len); + return; + } + #endif // KITTY_GRAPHICS_PATCH + strparse(); par = (narg = strescseq.narg) ? atoi(strescseq.args[0]) : 0; @@ -3381,6 +3410,19 @@ check_control_code: return; } + #if KITTY_GRAPHICS_PATCH + /* absorb row/column diacritics into the preceding placeholder cell */ + if (width == 0 && kitty_is_diacritic(u)) { + int px = term.c.x - ((term.c.state & CURSOR_WRAPNEXT) ? 0 : 1); + if (px >= 0 && px < term.col && + (term.line[term.c.y][px].mode & ATTR_KITTYPH) && + kitty_absorb_diacritic(&term.line[term.c.y][px], u)) { + term.dirty[term.c.y] = 1; + return; + } + } + #endif // KITTY_GRAPHICS_PATCH + #if REFLOW_PATCH /* selected() takes relative coordinates */ if (selected(term.c.x + term.scr, term.c.y + term.scr)) diff --git a/st.h b/st.h index 95d1be1..32012ed 100644 --- a/st.h +++ b/st.h @@ -69,6 +69,9 @@ enum glyph_attribute { #if KEYBOARDSELECT_PATCH && REFLOW_PATCH ATTR_HIGHLIGHT = 1 << 17, #endif // KEYBOARDSELECT_PATCH + #if KITTY_GRAPHICS_PATCH + ATTR_KITTYPH = 1 << 18, + #endif // KITTY_GRAPHICS_PATCH ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, }; @@ -144,6 +147,9 @@ typedef struct { int ustyle; /* underline style */ int ucolor[3]; /* underline color */ #endif // UNDERCURL_PATCH + #if KITTY_GRAPHICS_PATCH + uint32_t kitty; /* kitty Unicode placeholder row/column/msb */ + #endif // KITTY_GRAPHICS_PATCH } Glyph; typedef Glyph *Line; diff --git a/x.c b/x.c index 5940238..3330a94 100644 --- a/x.c +++ b/x.c @@ -60,6 +60,10 @@ static void zoomreset(const Arg *); #include "patch/st_include.h" #include "patch/x_include.h" +#if KITTY_GRAPHICS_PATCH +#include "kitty.h" +#endif // KITTY_GRAPHICS_PATCH + /* config.h for applying patches and the configuration. */ #include "config.h" @@ -332,6 +336,10 @@ zoomabs(const Arg *arg) } #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_resize(); + #endif // KITTY_GRAPHICS_PATCH + cresize(0, 0); redraw(); xhints(); @@ -3265,6 +3273,10 @@ xfinishdraw(void) } #endif // SIXEL_PATCH + #if KITTY_GRAPHICS_PATCH + kitty_draw(); + #endif // KITTY_GRAPHICS_PATCH + #if !SINGLE_DRAWABLE_BUFFER_PATCH XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, win.h, 0, 0); #endif // SINGLE_DRAWABLE_BUFFER_PATCH @@ -3770,6 +3782,13 @@ run(void) /* idle detected or maxlatency exhausted -> draw */ timeout = -1; + #if KITTY_GRAPHICS_PATCH + { + double animto = -1; + if (kitty_animating(&animto) && animto >= 0) + timeout = animto; + } + #endif // KITTY_GRAPHICS_PATCH #if BLINKING_CURSOR_PATCH if (blinktimeout && (cursorblinks || tattrset(ATTR_BLINK))) #else