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).
2567 lines
63 KiB
C
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
|