/* 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; uint32_t acc = 0; int 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) | (uint32_t)b64tab[c]) & 0xffffff; 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 */ /* ------------------------------------------------------------------ */ static int kitty_cmp(const void *a, const void *b); /* 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; } /* like kitty_blit but restricted to the cell span [cx1, cx2) on row cy */ static void kitty_blit_clipped(KittyPlacement *p, int px, int py, int cx1, int cx2, int cy) { Picture src, dstpic; XRenderPictFormat *fmt; XRenderPictureAttributes pa; int w, h, bx, by; int x0, x1, y0, y1; /* clip rect in window pixels */ int ix0, iy0; /* image origin in window pixels */ int sxo, syo, cw, chh; if (!kitty_prepare(p, &w, &h)) return; kitty_border(&bx, &by); ix0 = bx + px * win.cw + p->xoff; iy0 = by + (py - kitty_scr()) * win.ch + p->yoff; x0 = KMAX(ix0, bx + cx1 * win.cw); x1 = KMIN(ix0 + w, bx + cx2 * win.cw); y0 = KMAX(iy0, by + cy * win.ch); y1 = KMIN(iy0 + h, by + (cy + 1) * win.ch); x1 = KMIN(x1, bx + win.tw); y1 = KMIN(y1, by + win.th); if (x1 <= x0 || y1 <= y0) return; sxo = x0 - ix0; syo = y0 - iy0; cw = x1 - x0; chh = y1 - y0; 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) { dstpic = XRenderCreatePicture(xw.dpy, xw.buf, fmt, 0, &pa); XRenderComposite(xw.dpy, PictOpOver, src, None, dstpic, sxo, syo, 0, 0, x0, y0, cw, chh); XRenderFreePicture(xw.dpy, dstpic); } XRenderFreePicture(xw.dpy, src); } /* * Composite the negative z-index images covering the cell span * [x1, x2) on row y. Called after the background has been painted but * before the glyphs, so that text appears on top of the image. */ void kitty_draw_under_cells(int x1, int y, int x2) { KittyPlacement **list = NULL, *p; int n = 0, cap = 0, i; int s = IS_SET(MODE_ALTSCREEN) ? 1 : 0; int scr = kitty_scr(); for (p = placements[s]; p; p = p->next) { int px, py; if (p->virt || p->z >= 0) continue; if (!kitty_resolve_pos(p, &px, &py, 0)) continue; py -= scr; if (px >= x2 || px + p->cols <= x1 || py > y || py + p->rows <= y) continue; if (n >= cap) { int ncap = cap ? cap * 2 : 8; 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_clipped(list[i], px, py, x1, x2, y); } free(list); } 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; } } } /* * Draw the visible placements whose z-index falls in [zlo, zhi]. * Negative z-index images are drawn under the text (see kitty_draw_under). */ static void kitty_draw_range(int zlo, int zhi) { 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 (p->z < zlo || p->z > zhi) 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); } void kitty_draw(void) { kitty_draw_range(0, INT_MAX); /* images displayed via Unicode placeholders */ kitty_draw_placeholders(); } /* is the cell (x, y) covered by an image drawn under the text? */ int kitty_cell_under(int x, int y) { KittyPlacement *p; int s = IS_SET(MODE_ALTSCREEN) ? 1 : 0; int scr = kitty_scr(); for (p = placements[s]; p; p = p->next) { int px, py; if (p->virt || p->z >= 0) continue; if (!kitty_resolve_pos(p, &px, &py, 0)) continue; py -= scr; if (x >= px && x < px + p->cols && y >= py && y < py + p->rows) return 1; } return 0; } /* ------------------------------------------------------------------ */ /* 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