st/kitty.c
a cb1f41c9ae
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).
2026-09-06 20:41:24 -05:00

2567 lines
63 KiB
C

/* 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 <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xrender.h>
#include <Imlib2.h>
#include <zlib.h>
#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