feat: kitty images patch applied

This commit is contained in:
2026-04-15 13:46:38 +02:00
parent 688f70add0
commit 67c54ec28b
15 changed files with 10000 additions and 58 deletions

303
st.c
View File

@@ -19,6 +19,7 @@
#include "st.h"
#include "win.h"
#include "graphics.h"
#if defined(__linux)
#include <pty.h>
@@ -36,6 +37,10 @@
#define STR_BUF_SIZ ESC_BUF_SIZ
#define STR_ARG_SIZ ESC_ARG_SIZ
/* PUA character used as an image placeholder */
#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE
#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE
/* macros */
#define IS_SET(flag) ((term.mode & (flag)) != 0)
#define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f)
@@ -113,6 +118,8 @@ typedef struct {
typedef struct {
int row; /* nb row */
int col; /* nb col */
int pixw; /* width of the text area in pixels */
int pixh; /* height of the text area in pixels */
Line *line; /* screen */
Line *alt; /* alternate screen */
int *dirty; /* dirtyness of lines */
@@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *);
static char utf8encodebyte(Rune, size_t);
static size_t utf8validate(Rune *, size_t);
static char *base64dec(const char *);
static char base64dec_getc(const char **);
static ssize_t xwrite(int, const char *, size_t);
@@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0
* means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */
uint16_t diacritic_to_num(uint32_t code);
ssize_t
xwrite(int fd, const char *s, size_t len)
{
@@ -616,6 +626,12 @@ getsel(void)
if (gp->mode & ATTR_WDUMMY)
continue;
if (gp->mode & ATTR_IMAGE) {
// TODO: Copy diacritics as well
ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr);
continue;
}
ptr += utf8encode(gp->u, ptr);
}
@@ -715,11 +731,14 @@ sigchld(int a)
int stat;
pid_t p;
if ((p = waitpid(pid, &stat, WNOHANG)) < 0)
if ((p = waitpid(-1, &stat, WNOHANG)) < 0)
die("waiting for pid %hd failed: %s\n", pid, strerror(errno));
if (pid != p)
if (pid != p) {
/* reinstall sigchld handler */
signal(SIGCHLD, sigchld);
return;
}
if (WIFEXITED(stat) && WEXITSTATUS(stat))
die("child exited with status %d\n", WEXITSTATUS(stat));
@@ -819,7 +838,11 @@ ttyread(void)
{
static char buf[BUFSIZ];
static int buflen = 0;
int ret, written;
static int already_processing = 0;
int ret, written = 0;
if (buflen >= LEN(buf))
return 0;
/* append read bytes to unprocessed bytes */
ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
@@ -831,7 +854,24 @@ ttyread(void)
die("couldn't read from shell: %s\n", strerror(errno));
default:
buflen += ret;
written = twrite(buf, buflen, 0);
if (already_processing) {
/* Avoid recursive call to twrite() */
return ret;
}
already_processing = 1;
while (1) {
int buflen_before_processing = buflen;
written += twrite(buf + written, buflen - written, 0);
// If buflen changed during the call to twrite, there is
// new data, and we need to keep processing, otherwise
// we can exit. This will not loop forever because the
// buffer is limited, and we don't clean it in this
// loop, so at some point ttywrite will have to drop
// some data.
if (buflen_before_processing == buflen)
break;
}
already_processing = 0;
buflen -= written;
/* keep any incomplete UTF-8 byte sequence for the next call */
if (buflen > 0)
@@ -874,6 +914,7 @@ ttywriteraw(const char *s, size_t n)
fd_set wfd, rfd;
ssize_t r;
size_t lim = 256;
int retries_left = 100;
/*
* Remember that we are using a pty, which might be a modem line.
@@ -882,6 +923,9 @@ ttywriteraw(const char *s, size_t n)
* FIXME: Migrate the world to Plan 9.
*/
while (n > 0) {
if (retries_left-- <= 0)
goto too_many_retries;
FD_ZERO(&wfd);
FD_ZERO(&rfd);
FD_SET(cmdfd, &wfd);
@@ -923,11 +967,16 @@ ttywriteraw(const char *s, size_t n)
write_error:
die("write error on tty: %s\n", strerror(errno));
too_many_retries:
fprintf(stderr, "Could not write %zu bytes to tty\n", n);
}
void
ttyresize(int tw, int th)
{
term.pixw = tw;
term.pixh = th;
struct winsize w;
w.ws_row = term.row;
@@ -1018,7 +1067,8 @@ treset(void)
term.c = (TCursor){{
.mode = ATTR_NULL,
.fg = defaultfg,
.bg = defaultbg
.bg = defaultbg,
.decor = DECOR_DEFAULT_COLOR
}, .x = 0, .y = 0, .state = CURSOR_DEFAULT};
memset(term.tabs, 0, term.col * sizeof(*term.tabs));
@@ -1041,7 +1091,9 @@ treset(void)
void
tnew(int col, int row)
{
term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
term = (Term){.c = {.attr = {.fg = defaultfg,
.bg = defaultbg,
.decor = DECOR_DEFAULT_COLOR}}};
tresize(col, row);
treset();
}
@@ -1218,9 +1270,24 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
term.line[y][x-1].mode &= ~ATTR_WIDE;
}
if (u == ' ' && term.line[y][x].mode & ATTR_IMAGE &&
tgetisclassicplaceholder(&term.line[y][x])) {
// This is a workaround: don't overwrite classic placement
// placeholders with space symbols (unlike Unicode placeholders
// which must be overwritten by anything).
term.line[y][x].bg = attr->bg;
term.dirty[y] = 1;
return;
}
term.dirty[y] = 1;
term.line[y][x] = *attr;
term.line[y][x].u = u;
if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) {
term.line[y][x].u = 0;
term.line[y][x].mode |= ATTR_IMAGE;
}
}
void
@@ -1247,12 +1314,110 @@ tclearregion(int x1, int y1, int x2, int y2)
selclear();
gp->fg = term.c.attr.fg;
gp->bg = term.c.attr.bg;
gp->decor = term.c.attr.decor;
gp->mode = 0;
gp->u = ' ';
}
}
}
/// Fills a rectangle area with an image placeholder. The starting point is the
/// cursor. Adds empty lines if needed. The placeholder will be marked as
/// classic.
void tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id, int cols,
int rows, char do_not_move_cursor,
Glyph *text_underneath) {
for (int row = 0; row < rows; ++row) {
int y = term.c.y;
term.dirty[y] = 1;
for (int col = 0; col < cols; ++col) {
int x = term.c.x + col;
if (x >= term.col)
break;
Glyph *gp = &term.line[y][x];
if (selected(x, y))
selclear();
if (text_underneath) {
Glyph *to_save = gp;
// If there is already a classic placeholder,
// use the text underneath it. This will leave
// holes in images, but at least we are
// guaranteed to restore the original text.
if (gp->mode & ATTR_IMAGE &&
tgetisclassicplaceholder(gp)) {
Glyph *under =
gr_get_glyph_underneath_image(
tgetimgid(gp),
tgetimgplacementid(gp),
tgetimgcol(gp),
tgetimgrow(gp));
if (under)
to_save = under;
}
text_underneath[cols * row + col] = *to_save;
}
gp->mode = ATTR_IMAGE;
gp->u = 0;
tsetimgrow(gp, row + 1);
tsetimgcol(gp, col + 1);
tsetimgid(gp, image_id);
tsetimgplacementid(gp, placement_id);
tsetimgdiacriticcount(gp, 3);
tsetisclassicplaceholder(gp, 1);
}
// If moving the cursor is not allowed and this is the last line
// of the terminal, we are done.
if (do_not_move_cursor && y == term.row - 1)
break;
// Move the cursor down, maybe creating a new line. The x is
// preserved (we never change term.c.x in the loop above).
if (row != rows - 1)
tnewline(/*first_col=*/0);
}
if (do_not_move_cursor) {
// Return the cursor to the original position.
tmoveto(term.c.x, term.c.y - rows + 1);
} else {
// Move the cursor beyond the last column, as required by the
// protocol. If the cursor goes beyond the screen edge, insert a
// newline to match the behavior of kitty.
if (term.c.x + cols >= term.col)
tnewline(/*first_col=*/1);
else
tmoveto(term.c.x + cols, term.c.y);
}
}
void gr_for_each_image_cell(int (*callback)(void *data, Glyph *gp),
void *data) {
for (int row = 0; row < term.row; ++row) {
for (int col = 0; col < term.col; ++col) {
Glyph *gp = &term.line[row][col];
if (gp->mode & ATTR_IMAGE) {
if (callback(data, gp))
term.dirty[row] = 1;
}
}
}
}
void gr_schedule_image_redraw_by_id(uint32_t image_id) {
for (int row = 0; row < term.row; ++row) {
if (term.dirty[row])
continue;
for (int col = 0; col < term.col; ++col) {
Glyph *gp = &term.line[row][col];
if (gp->mode & ATTR_IMAGE) {
uint32_t cell_image_id = tgetimgid(gp);
if (cell_image_id == image_id) {
term.dirty[row] = 1;
break;
}
}
}
}
}
void
tdeletechar(int n)
{
@@ -1371,6 +1536,7 @@ tsetattr(const int *attr, int l)
ATTR_STRUCK );
term.c.attr.fg = defaultfg;
term.c.attr.bg = defaultbg;
term.c.attr.decor = DECOR_DEFAULT_COLOR;
break;
case 1:
term.c.attr.mode |= ATTR_BOLD;
@@ -1383,6 +1549,20 @@ tsetattr(const int *attr, int l)
break;
case 4:
term.c.attr.mode |= ATTR_UNDERLINE;
if (i + 1 < l) {
idx = attr[++i];
if (BETWEEN(idx, 1, 5)) {
tsetdecorstyle(&term.c.attr, idx);
} else if (idx == 0) {
term.c.attr.mode &= ~ATTR_UNDERLINE;
tsetdecorstyle(&term.c.attr, 0);
} else {
fprintf(stderr,
"erresc: unknown underline "
"style %d\n",
idx);
}
}
break;
case 5: /* slow blink */
/* FALLTHROUGH */
@@ -1406,6 +1586,7 @@ tsetattr(const int *attr, int l)
break;
case 24:
term.c.attr.mode &= ~ATTR_UNDERLINE;
tsetdecorstyle(&term.c.attr, 0);
break;
case 25:
term.c.attr.mode &= ~ATTR_BLINK;
@@ -1434,10 +1615,11 @@ tsetattr(const int *attr, int l)
term.c.attr.bg = defaultbg;
break;
case 58:
/* This starts a sequence to change the color of
* "underline" pixels. We don't support that and
* instead eat up a following "5;n" or "2;r;g;b". */
tdefcolor(attr, &i, l);
if ((idx = tdefcolor(attr, &i, l)) >= 0)
tsetdecorcolor(&term.c.attr, idx);
break;
case 59:
tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR);
break;
default:
if (BETWEEN(attr[i], 30, 37)) {
@@ -1826,6 +2008,39 @@ csihandle(void)
goto unknown;
}
break;
case '>':
switch (csiescseq.mode[1]) {
case 'q': /* XTVERSION -- Print terminal name and version */
len = snprintf(buf, sizeof(buf),
"\033P>|st-graphics(%s)\033\\", VERSION);
ttywrite(buf, len, 0);
break;
default:
goto unknown;
}
break;
case 't': /* XTWINOPS -- Window manipulation */
switch (csiescseq.arg[0]) {
case 14: /* Report text area size in pixels. */
len = snprintf(buf, sizeof(buf), "\033[4;%i;%it",
term.pixh, term.pixw);
ttywrite(buf, len, 0);
break;
case 16: /* Report character cell size in pixels. */
len = snprintf(buf, sizeof(buf), "\033[6;%i;%it",
term.pixh / term.row,
term.pixw / term.col);
ttywrite(buf, len, 0);
break;
case 18: /* Report the size of the text area in characters. */
len = snprintf(buf, sizeof(buf), "\033[8;%i;%it",
term.row, term.col);
ttywrite(buf, len, 0);
break;
default:
goto unknown;
}
break;
}
}
@@ -1988,8 +2203,27 @@ strhandle(void)
case 'k': /* old title set compatibility */
xsettitle(strescseq.args[0]);
return;
case 'P': /* DCS -- Device Control String */
case '_': /* APC -- Application Program Command */
if (gr_parse_command(strescseq.buf, strescseq.len)) {
GraphicsCommandResult *res = &graphics_command_result;
if (res->create_placeholder) {
tcreateimgplaceholder(
res->placeholder.image_id,
res->placeholder.placement_id,
res->placeholder.columns,
res->placeholder.rows,
res->placeholder.do_not_move_cursor,
res->placeholder.text_underneath);
}
if (res->response[0])
ttywrite(res->response, strlen(res->response),
0);
if (res->redraw)
tfulldirt();
return;
}
return;
case 'P': /* DCS -- Device Control String */
case '^': /* PM -- Privacy Message */
return;
}
@@ -2496,6 +2730,41 @@ check_control_code:
if (selected(term.c.x, term.c.y))
selclear();
// wcwidth is broken on some systems, set the width to 0 if it's a known
// diacritic used for images.
uint16_t num = diacritic_to_num(u);
if (num != 0)
width = 0;
// Set the width to 1 if it's an image placeholder character.
if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD)
width = 1;
if (width == 0) {
// It's probably a combining char. Combining characters are not
// supported, so we just ignore them, unless it denotes the row and
// column of an image character.
if (term.c.y <= 0 && term.c.x <= 0)
return;
else if (term.c.x == 0)
gp = &term.line[term.c.y-1][term.col-1];
else if (term.c.state & CURSOR_WRAPNEXT)
gp = &term.line[term.c.y][term.c.x];
else
gp = &term.line[term.c.y][term.c.x-1];
if (num && (gp->mode & ATTR_IMAGE)) {
unsigned diaccount = tgetimgdiacriticcount(gp);
if (diaccount == 0)
tsetimgrow(gp, num);
else if (diaccount == 1)
tsetimgcol(gp, num);
else if (diaccount == 2)
tsetimg4thbyteplus1(gp, num);
tsetimgdiacriticcount(gp, diaccount + 1);
}
term.lastc = u;
return;
}
gp = &term.line[term.c.y][term.c.x];
if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) {
gp->mode |= ATTR_WRAP;
@@ -2662,6 +2931,8 @@ drawregion(int x1, int y1, int x2, int y2)
{
int y;
xstartimagedraw(term.dirty, term.row);
for (y = y1; y < y2; y++) {
if (!term.dirty[y])
continue;
@@ -2669,6 +2940,8 @@ drawregion(int x1, int y1, int x2, int y2)
term.dirty[y] = 0;
xdrawline(term.line[y], x1, y, x2);
}
xfinishimagedraw();
}
void
@@ -2703,3 +2976,9 @@ redraw(void)
tfulldirt();
draw();
}
Glyph
getglyphat(int col, int row)
{
return term.line[row][col];
}