aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--patch-st-2805251548
1 files changed, 1548 insertions, 0 deletions
diff --git a/patch-st-280525 b/patch-st-280525
new file mode 100644
index 0000000..135b6fc
--- /dev/null
+++ b/patch-st-280525
@@ -0,0 +1,1548 @@
+diff -u /src/MASTER/st/config.def.h /src/WORKING/st/config.def.h
+--- /src/MASTER/st/config.def.h 2025-05-01 17:34:28.799888436 +0200
++++ /src/WORKING/st/config.def.h 2025-05-21 14:09:06.805450577 +0200
+@@ -8,6 +8,13 @@
+ static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true";
+ static int borderpx = 2;
+
++/* How to align the content in the window when the size of the terminal
++ * doesn't perfectly match the size of the window. The values are percentages.
++ * 50 means center, 0 means flush left/top, 100 means flush right/bottom.
++ */
++static int anysize_halign = 50;
++static int anysize_valign = 50;
++
+ /*
+ * What program is execed by st depends of these precedence rules:
+ * 1: program passed with -e
+@@ -23,7 +30,8 @@
+ char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400";
+
+ /* identification sequence returned in DA and DECID */
+-char *vtiden = "\033[?6c";
++/* By default, use the same one as kitty. */
++char *vtiden = "\033[?62c";
+
+ /* Kerning / character bounding-box multipliers */
+ static float cwscale = 1.0;
+@@ -164,18 +172,46 @@
+ static unsigned int defaultattr = 11;
+
+ /*
++ * Graphics configuration
++ */
++
++/// The template for the cache directory.
++const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX";
++/// The max size of a single image file, in bytes.
++unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024;
++/// The max size of the cache, in bytes.
++unsigned graphics_total_file_cache_size = 300 * 1024 * 1024;
++/// The max ram size of an image or placement, in bytes.
++unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024;
++/// The max total size of all images loaded into RAM.
++unsigned graphics_max_total_ram_size = 300 * 1024 * 1024;
++/// The max total number of image placements and images.
++unsigned graphics_max_total_placements = 4096;
++/// The ratio by which limits can be exceeded. This is to reduce the frequency
++/// of image removal.
++double graphics_excess_tolerance_ratio = 0.05;
++/// The minimum delay between redraws caused by animations, in milliseconds.
++unsigned graphics_animation_min_delay = 20;
++
++/*
+ * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set).
+ * Note that if you want to use ShiftMask with selmasks, set this to an other
+ * modifier, set to 0 to not use it.
+ */
+ static uint forcemousemod = ShiftMask;
+
++/* Internal keyboard shortcuts. */
++#define MODKEY Mod1Mask
++#define TERMMOD (ControlMask|ShiftMask)
++
+ /*
+ * Internal mouse shortcuts.
+ * Beware that overloading Button1 will disable the selection.
+ */
+ static MouseShortcut mshortcuts[] = {
+ /* mask button function argument release */
++ { TERMMOD, Button3, previewimage, {.s = "feh"} },
++ { TERMMOD, Button2, showimageinfo, {}, 1 },
+ { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
+ { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
+ { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
+@@ -183,10 +219,6 @@
+ { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} },
+ };
+
+-/* Internal keyboard shortcuts. */
+-#define MODKEY Mod1Mask
+-#define TERMMOD (ControlMask|ShiftMask)
+-
+ static Shortcut shortcuts[] = {
+ /* mask keysym function argument */
+ { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
+@@ -201,6 +233,10 @@
+ { TERMMOD, XK_Y, selpaste, {.i = 0} },
+ { ShiftMask, XK_Insert, selpaste, {.i = 0} },
+ { TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
++ { TERMMOD, XK_F1, togglegrdebug, {.i = 0} },
++ { TERMMOD, XK_F6, dumpgrstate, {.i = 0} },
++ { TERMMOD, XK_F7, unloadimages, {.i = 0} },
++ { TERMMOD, XK_F8, toggleimages, {.i = 0} },
+ };
+
+ /*
+Only in /src/WORKING/st/: config.h
+diff -u /src/MASTER/st/config.mk /src/WORKING/st/config.mk
+--- /src/MASTER/st/config.mk 2025-05-01 17:34:28.799888436 +0200
++++ /src/WORKING/st/config.mk 2025-05-21 14:09:06.805450577 +0200
+@@ -14,9 +14,12 @@
+
+ # includes and libs
+ INCS = -I$(X11INC) \
++ `$(PKG_CONFIG) --cflags imlib2` \
+ `$(PKG_CONFIG) --cflags fontconfig` \
+ `$(PKG_CONFIG) --cflags freetype2`
+-LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
++LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender \
++ `$(PKG_CONFIG) --libs imlib2` \
++ `$(PKG_CONFIG) --libs zlib` \
+ `$(PKG_CONFIG) --libs fontconfig` \
+ `$(PKG_CONFIG) --libs freetype2`
+
+Only in /src/MASTER/st/: .git
+Only in /src/WORKING/st/: graphics.c
+Only in /src/WORKING/st/: graphics.h
+Only in /src/WORKING/st/: icat-mini.sh
+Only in /src/WORKING/st/: khash.h
+Only in /src/WORKING/st/: kvec.h
+diff -u /src/MASTER/st/Makefile /src/WORKING/st/Makefile
+--- /src/MASTER/st/Makefile 2025-05-01 17:34:28.799888436 +0200
++++ /src/WORKING/st/Makefile 2025-05-21 14:09:06.805450577 +0200
+@@ -4,7 +4,7 @@
+
+ include config.mk
+
+-SRC = st.c x.c
++SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c
+ OBJ = $(SRC:.c=.o)
+
+ all: st
+@@ -16,7 +16,7 @@
+ $(CC) $(STCFLAGS) -c $<
+
+ st.o: config.h st.h win.h
+-x.o: arg.h config.h st.h win.h
++x.o: arg.h config.h st.h win.h graphics.h
+
+ $(OBJ): config.h config.mk
+
+Only in /src/WORKING/st/: rowcolumn_diacritics_helpers.c
+diff -u /src/MASTER/st/st.c /src/WORKING/st/st.c
+--- /src/MASTER/st/st.c 2025-05-01 17:34:28.803221774 +0200
++++ /src/WORKING/st/st.c 2025-05-21 14:09:06.808783912 +0200
+@@ -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 {
+ 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 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 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 @@
+ 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);
+ }
+
+@@ -819,7 +835,11 @@
+ {
+ 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 +851,24 @@
+ 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 +911,7 @@
+ 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 +920,9 @@
+ * 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 +964,16 @@
+
+ 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;
+@@ -1015,7 +1061,8 @@
+ 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));
+@@ -1038,7 +1085,9 @@
+ 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();
+ }
+@@ -1215,9 +1264,24 @@
+ 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
+@@ -1244,12 +1308,104 @@
+ 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)
++{
++ 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();
++ 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, uint32_t image_id,
++ uint32_t placement_id, int col,
++ int row, char is_classic),
++ 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) {
++ uint32_t image_id = tgetimgid(gp);
++ uint32_t placement_id = tgetimgplacementid(gp);
++ int ret =
++ callback(data, tgetimgid(gp),
++ tgetimgplacementid(gp),
++ tgetimgcol(gp), tgetimgrow(gp),
++ tgetisclassicplaceholder(gp));
++ if (ret == 1) {
++ term.dirty[row] = 1;
++ gp->mode = 0;
++ gp->u = ' ';
++ }
++ }
++ }
++ }
++}
++
++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)
+ {
+@@ -1368,6 +1524,7 @@
+ 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;
+@@ -1380,6 +1537,20 @@
+ 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 */
+@@ -1403,6 +1574,7 @@
+ break;
+ case 24:
+ term.c.attr.mode &= ~ATTR_UNDERLINE;
++ tsetdecorstyle(&term.c.attr, 0);
+ break;
+ case 25:
+ term.c.attr.mode &= ~ATTR_BLINK;
+@@ -1430,6 +1602,13 @@
+ case 49:
+ term.c.attr.bg = defaultbg;
+ break;
++ case 58:
++ 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)) {
+ term.c.attr.fg = attr[i] - 30;
+@@ -1817,6 +1996,39 @@
+ 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;
+ }
+ }
+
+@@ -1966,8 +2178,26 @@
+ 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);
++ }
++ 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;
+ }
+@@ -2473,6 +2703,33 @@
+ if (selected(term.c.x, term.c.y))
+ selclear();
+
++ 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];
++ uint16_t num = diacritic_to_num(u);
++ 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;
+@@ -2639,6 +2896,8 @@
+ {
+ int y;
+
++ xstartimagedraw(term.dirty, term.row);
++
+ for (y = y1; y < y2; y++) {
+ if (!term.dirty[y])
+ continue;
+@@ -2646,6 +2905,8 @@
+ term.dirty[y] = 0;
+ xdrawline(term.line[y], x1, y, x2);
+ }
++
++ xfinishimagedraw();
+ }
+
+ void
+@@ -2680,3 +2941,9 @@
+ tfulldirt();
+ draw();
+ }
++
++Glyph
++getglyphat(int col, int row)
++{
++ return term.line[row][col];
++}
+diff -u /src/MASTER/st/st.h /src/WORKING/st/st.h
+--- /src/MASTER/st/st.h 2025-05-01 17:34:28.803221774 +0200
++++ /src/WORKING/st/st.h 2025-05-21 14:09:06.808783912 +0200
+@@ -12,7 +12,7 @@
+ #define DEFAULT(a, b) (a) = (a) ? (a) : (b)
+ #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
+ #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
+- (a).bg != (b).bg)
++ (a).bg != (b).bg || (a).decor != (b).decor)
+ #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
+ (t1.tv_nsec-t2.tv_nsec)/1E6)
+ #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit)))
+@@ -20,6 +20,10 @@
+ #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b))
+ #define IS_TRUECOL(x) (1 << 24 & (x))
+
++// This decor color indicates that the fg color should be used. Note that it's
++// not a 24-bit color because the 25-th bit is not set.
++#define DECOR_DEFAULT_COLOR 0x0ffffff
++
+ enum glyph_attribute {
+ ATTR_NULL = 0,
+ ATTR_BOLD = 1 << 0,
+@@ -34,6 +38,7 @@
+ ATTR_WIDE = 1 << 9,
+ ATTR_WDUMMY = 1 << 10,
+ ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
++ ATTR_IMAGE = 1 << 14,
+ };
+
+ enum selection_mode {
+@@ -52,6 +57,14 @@
+ SNAP_LINE = 2
+ };
+
++enum underline_style {
++ UNDERLINE_STRAIGHT = 1,
++ UNDERLINE_DOUBLE = 2,
++ UNDERLINE_CURLY = 3,
++ UNDERLINE_DOTTED = 4,
++ UNDERLINE_DASHED = 5,
++};
++
+ typedef unsigned char uchar;
+ typedef unsigned int uint;
+ typedef unsigned long ulong;
+@@ -65,6 +78,7 @@
+ ushort mode; /* attribute flags */
+ uint32_t fg; /* foreground */
+ uint32_t bg; /* background */
++ uint32_t decor; /* decoration (like underline) */
+ } Glyph;
+
+ typedef Glyph *Line;
+@@ -105,6 +119,8 @@
+ int selected(int, int);
+ char *getsel(void);
+
++Glyph getglyphat(int, int);
++
+ size_t utf8encode(Rune, char *);
+
+ void *xmalloc(size_t);
+@@ -124,3 +140,69 @@
+ extern unsigned int defaultfg;
+ extern unsigned int defaultbg;
+ extern unsigned int defaultcs;
++
++// Accessors to decoration properties stored in `decor`.
++// The 25-th bit is used to indicate if it's a 24-bit color.
++static inline uint32_t tgetdecorcolor(Glyph *g) { return g->decor & 0x1ffffff; }
++static inline uint32_t tgetdecorstyle(Glyph *g) { return (g->decor >> 25) & 0x7; }
++static inline void tsetdecorcolor(Glyph *g, uint32_t color) {
++ g->decor = (g->decor & ~0x1ffffff) | (color & 0x1ffffff);
++}
++static inline void tsetdecorstyle(Glyph *g, uint32_t style) {
++ g->decor = (g->decor & ~(0x7 << 25)) | ((style & 0x7) << 25);
++}
++
++
++// Some accessors to image placeholder properties stored in `u`:
++// - row (1-base) - 9 bits
++// - column (1-base) - 9 bits
++// - most significant byte of the image id plus 1 - 9 bits (0 means unspecified,
++// don't forget to subtract 1).
++// - the original number of diacritics (0, 1, 2, or 3) - 2 bits
++// - whether this is a classic (1) or Unicode (0) placeholder - 1 bit
++static inline uint32_t tgetimgrow(Glyph *g) { return g->u & 0x1ff; }
++static inline uint32_t tgetimgcol(Glyph *g) { return (g->u >> 9) & 0x1ff; }
++static inline uint32_t tgetimgid4thbyteplus1(Glyph *g) { return (g->u >> 18) & 0x1ff; }
++static inline uint32_t tgetimgdiacriticcount(Glyph *g) { return (g->u >> 27) & 0x3; }
++static inline uint32_t tgetisclassicplaceholder(Glyph *g) { return (g->u >> 29) & 0x1; }
++static inline void tsetimgrow(Glyph *g, uint32_t row) {
++ g->u = (g->u & ~0x1ff) | (row & 0x1ff);
++}
++static inline void tsetimgcol(Glyph *g, uint32_t col) {
++ g->u = (g->u & ~(0x1ff << 9)) | ((col & 0x1ff) << 9);
++}
++static inline void tsetimg4thbyteplus1(Glyph *g, uint32_t byteplus1) {
++ g->u = (g->u & ~(0x1ff << 18)) | ((byteplus1 & 0x1ff) << 18);
++}
++static inline void tsetimgdiacriticcount(Glyph *g, uint32_t count) {
++ g->u = (g->u & ~(0x3 << 27)) | ((count & 0x3) << 27);
++}
++static inline void tsetisclassicplaceholder(Glyph *g, uint32_t isclassic) {
++ g->u = (g->u & ~(0x1 << 29)) | ((isclassic & 0x1) << 29);
++}
++
++/// Returns the full image id. This is a naive implementation, if the most
++/// significant byte is not specified, it's assumed to be 0 instead of inferring
++/// it from the cells to the left.
++static inline uint32_t tgetimgid(Glyph *g) {
++ uint32_t msb = tgetimgid4thbyteplus1(g);
++ if (msb != 0)
++ --msb;
++ return (msb << 24) | (g->fg & 0xFFFFFF);
++}
++
++/// Sets the full image id.
++static inline void tsetimgid(Glyph *g, uint32_t id) {
++ g->fg = (id & 0xFFFFFF) | (1 << 24);
++ tsetimg4thbyteplus1(g, ((id >> 24) & 0xFF) + 1);
++}
++
++static inline uint32_t tgetimgplacementid(Glyph *g) {
++ if (tgetdecorcolor(g) == DECOR_DEFAULT_COLOR)
++ return 0;
++ return g->decor & 0xFFFFFF;
++}
++
++static inline void tsetimgplacementid(Glyph *g, uint32_t id) {
++ g->decor = (id & 0xFFFFFF) | (1 << 24);
++}
+diff -u /src/MASTER/st/st.info /src/WORKING/st/st.info
+--- /src/MASTER/st/st.info 2025-05-01 17:34:28.803221774 +0200
++++ /src/WORKING/st/st.info 2025-05-21 14:09:06.808783912 +0200
+@@ -195,6 +195,7 @@
+ Ms=\E]52;%p1%s;%p2%s\007,
+ Se=\E[2 q,
+ Ss=\E[%p1%d q,
++ Smulx=\E[4:%p1%dm,
+
+ st| simpleterm,
+ use=st-mono,
+@@ -215,6 +216,11 @@
+ initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
+ setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
+ setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
++# Underline colors
++ Su,
++ Setulc=\E[58:2:%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
++ Setulc1=\E[58:5:%p1%dm,
++ ol=\E[59m,
+
+ st-meta| simpleterm with meta key,
+ use=st,
+diff -u /src/MASTER/st/win.h /src/WORKING/st/win.h
+--- /src/MASTER/st/win.h 2025-05-01 17:34:28.803221774 +0200
++++ /src/WORKING/st/win.h 2025-05-21 14:09:06.808783912 +0200
+@@ -39,3 +39,6 @@
+ void xsetsel(char *);
+ int xstartdraw(void);
+ void xximspot(int, int);
++
++void xstartimagedraw(int *dirty, int rows);
++void xfinishimagedraw();
+diff -u /src/MASTER/st/x.c /src/WORKING/st/x.c
+--- /src/MASTER/st/x.c 2025-05-01 17:34:28.803221774 +0200
++++ /src/WORKING/st/x.c 2025-05-21 14:09:12.325454989 +0200
+@@ -4,6 +4,8 @@
+ #include <limits.h>
+ #include <locale.h>
+ #include <signal.h>
++#include <stdio.h>
++#include <stdlib.h>
+ #include <sys/select.h>
+ #include <time.h>
+ #include <unistd.h>
+@@ -14,11 +16,13 @@
+ #include <X11/keysym.h>
+ #include <X11/Xft/Xft.h>
+ #include <X11/XKBlib.h>
++#include <X11/Xresource.h>
+
+ char *argv0;
+ #include "arg.h"
+ #include "st.h"
+ #include "win.h"
++#include "graphics.h"
+
+ /* types used in config.h */
+ typedef struct {
+@@ -59,6 +63,12 @@
+ static void zoomabs(const Arg *);
+ static void zoomreset(const Arg *);
+ static void ttysend(const Arg *);
++static void previewimage(const Arg *);
++static void showimageinfo(const Arg *);
++static void togglegrdebug(const Arg *);
++static void dumpgrstate(const Arg *);
++static void unloadimages(const Arg *);
++static void toggleimages(const Arg *);
+
+ /* config.h for applying patches and the configuration. */
+ #include "config.h"
+@@ -81,6 +91,7 @@
+ typedef struct {
+ int tw, th; /* tty width and height */
+ int w, h; /* window width and height */
++ int hborderpx, vborderpx;
+ int ch; /* char height */
+ int cw; /* char width */
+ int mode; /* window state/mode flags */
+@@ -144,6 +155,8 @@
+ static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
+ static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
+ static void xdrawglyph(Glyph, int, int);
++static void xdrawimages(Glyph, Line, int x1, int y1, int x2);
++static void xdrawoneimagecell(Glyph, int x, int y);
+ static void xclear(int, int, int, int);
+ static int xgeommasktogravity(int);
+ static int ximopen(Display *);
+@@ -220,6 +233,7 @@
+ static XWindow xw;
+ static XSelection xsel;
+ static TermWindow win;
++static unsigned int mouse_col = 0, mouse_row = 0;
+
+ /* Font Ring Cache */
+ enum {
+@@ -328,10 +342,72 @@
+ ttywrite(arg->s, strlen(arg->s), 1);
+ }
+
++void
++previewimage(const Arg *arg)
++{
++ Glyph g = getglyphat(mouse_col, mouse_row);
++ if (g.mode & ATTR_IMAGE) {
++ uint32_t image_id = tgetimgid(&g);
++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n",
++ image_id, tgetimgplacementid(&g), tgetimgcol(&g),
++ tgetimgrow(&g));
++ gr_preview_image(image_id, arg->s);
++ }
++}
++
++void
++showimageinfo(const Arg *arg)
++{
++ Glyph g = getglyphat(mouse_col, mouse_row);
++ if (g.mode & ATTR_IMAGE) {
++ uint32_t image_id = tgetimgid(&g);
++ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n",
++ image_id, tgetimgplacementid(&g), tgetimgcol(&g),
++ tgetimgrow(&g));
++ char stcommand[256] = {0};
++ size_t len = snprintf(stcommand, sizeof(stcommand), "%s -e less", argv0);
++ if (len > sizeof(stcommand) - 1) {
++ fprintf(stderr, "Executable name too long: %s\n",
++ argv0);
++ return;
++ }
++ gr_show_image_info(image_id, tgetimgplacementid(&g),
++ tgetimgcol(&g), tgetimgrow(&g),
++ tgetisclassicplaceholder(&g),
++ tgetimgdiacriticcount(&g), argv0);
++ }
++}
++
++void
++togglegrdebug(const Arg *arg)
++{
++ graphics_debug_mode = (graphics_debug_mode + 1) % 3;
++ redraw();
++}
++
++void
++dumpgrstate(const Arg *arg)
++{
++ gr_dump_state();
++}
++
++void
++unloadimages(const Arg *arg)
++{
++ gr_unload_images_to_reduce_ram();
++}
++
++void
++toggleimages(const Arg *arg)
++{
++ graphics_display_images = !graphics_display_images;
++ redraw();
++}
++
+ int
+ evcol(XEvent *e)
+ {
+- int x = e->xbutton.x - borderpx;
++ int x = e->xbutton.x - win.hborderpx;
+ LIMIT(x, 0, win.tw - 1);
+ return x / win.cw;
+ }
+@@ -339,7 +415,7 @@
+ int
+ evrow(XEvent *e)
+ {
+- int y = e->xbutton.y - borderpx;
++ int y = e->xbutton.y - win.vborderpx;
+ LIMIT(y, 0, win.th - 1);
+ return y / win.ch;
+ }
+@@ -452,6 +528,9 @@
+ /* ignore Button<N>mask for Button<N> - it's set on release */
+ uint state = e->xbutton.state & ~buttonmask(e->xbutton.button);
+
++ mouse_col = evcol(e);
++ mouse_row = evrow(e);
++
+ for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) {
+ if (ms->release == release &&
+ ms->button == e->xbutton.button &&
+@@ -739,6 +818,9 @@
+ col = MAX(1, col);
+ row = MAX(1, row);
+
++ win.hborderpx = (win.w - col * win.cw) * anysize_halign / 100;
++ win.vborderpx = (win.h - row * win.ch) * anysize_valign / 100;
++
+ tresize(col, row);
+ xresize(col, row);
+ ttyresize(win.tw, win.th);
+@@ -869,8 +951,8 @@
+ sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize;
+ sizeh->height = win.h;
+ sizeh->width = win.w;
+- sizeh->height_inc = win.ch;
+- sizeh->width_inc = win.cw;
++ sizeh->height_inc = 1;
++ sizeh->width_inc = 1;
+ sizeh->base_height = 2 * borderpx;
+ sizeh->base_width = 2 * borderpx;
+ sizeh->min_height = win.ch + 2 * borderpx;
+@@ -1014,7 +1096,8 @@
+ FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12);
+ usedfontsize = 12;
+ }
+- defaultfontsize = usedfontsize;
++ if (defaultfontsize <= 0)
++ defaultfontsize = usedfontsize;
+ }
+
+ if (xloadfont(&dc.font, pattern))
+@@ -1024,7 +1107,7 @@
+ FcPatternGetDouble(dc.font.match->pattern,
+ FC_PIXEL_SIZE, 0, &fontval);
+ usedfontsize = fontval;
+- if (fontsize == 0)
++ if (defaultfontsize <= 0 && fontsize == 0)
+ defaultfontsize = fontval;
+ }
+
+@@ -1152,8 +1235,8 @@
+ xloadcols();
+
+ /* adjust fixed window geometry */
+- win.w = 2 * borderpx + cols * win.cw;
+- win.h = 2 * borderpx + rows * win.ch;
++ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw;
++ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch;
+ if (xw.gm & XNegative)
+ xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2;
+ if (xw.gm & YNegative)
+@@ -1240,12 +1323,15 @@
+ xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0);
+ if (xsel.xtarget == None)
+ xsel.xtarget = XA_STRING;
++
++ // Initialize the graphics (image display) module.
++ gr_init(xw.dpy, xw.vis, xw.cmap);
+ }
+
+ int
+ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
+ {
+- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
++ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp;
+ ushort mode, prevmode = USHRT_MAX;
+ Font *font = &dc.font;
+ int frcflags = FRC_NORMAL;
+@@ -1267,6 +1353,11 @@
+ if (mode == ATTR_WDUMMY)
+ continue;
+
++ /* Draw spaces for image placeholders (images will be drawn
++ * separately). */
++ if (mode & ATTR_IMAGE)
++ rune = ' ';
++
+ /* Determine font for glyph if different from previous glyph. */
+ if (prevmode != mode) {
+ prevmode = mode;
+@@ -1374,11 +1465,61 @@
+ return numspecs;
+ }
+
++/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen`
++ * is the length of the dash plus the length of the gap. `fraction` is the
++ * fraction of the dash length compared to `wavelen`. */
++static void
++xdrawunderdashed(Draw draw, Color *color, int x, int y, int w,
++ int wavelen, float fraction, int thick)
++{
++ int dashw = MAX(1, fraction * wavelen);
++ for (int i = x - x % wavelen; i < x + w; i += wavelen) {
++ int startx = MAX(i, x);
++ int endx = MIN(i + dashw, x + w);
++ if (startx < endx)
++ XftDrawRect(xw.draw, color, startx, y, endx - startx,
++ thick);
++ }
++}
++
++/* Draws an undercurl. `h` is the total height, including line thickness. */
++static void
++xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick)
++{
++ XGCValues gcvals = {.foreground = color->pixel,
++ .line_width = thick,
++ .line_style = LineSolid,
++ .cap_style = CapRound};
++ GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw),
++ GCForeground | GCLineWidth | GCLineStyle | GCCapStyle,
++ &gcvals);
++
++ XRectangle clip = {.x = x, .y = y, .width = w, .height = h};
++ XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted);
++
++ int yoffset = thick / 2;
++ int segh = MAX(1, h - thick);
++ /* Make sure every segment is at a 45 degree angle, otherwise it doesn't
++ * look good without antialiasing. */
++ int segw = segh;
++ int wavelen = MAX(1, segw * 2);
++
++ for (int i = x - (x % wavelen); i < x + w; i += wavelen) {
++ XPoint points[3] = {{.x = i, .y = y + yoffset},
++ {.x = i + segw, .y = y + yoffset + segh},
++ {.x = i + wavelen, .y = y + yoffset}};
++ XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3,
++ CoordModeOrigin);
++ }
++
++ XFreeGC(xw.dpy, gc);
++}
++
+ void
+ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y)
+ {
+ int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
+- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
++ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch,
+ width = charlen * win.cw;
+ Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
+ XRenderColor colfg, colbg;
+@@ -1468,17 +1609,17 @@
+
+ /* Intelligent cleaning up of the borders. */
+ if (x == 0) {
+- xclear(0, (y == 0)? 0 : winy, borderpx,
++ xclear(0, (y == 0)? 0 : winy, win.hborderpx,
+ winy + win.ch +
+- ((winy + win.ch >= borderpx + win.th)? win.h : 0));
++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0));
+ }
+- if (winx + width >= borderpx + win.tw) {
++ if (winx + width >= win.hborderpx + win.tw) {
+ xclear(winx + width, (y == 0)? 0 : winy, win.w,
+- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch)));
++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch)));
+ }
+ if (y == 0)
+- xclear(winx, 0, winx + width, borderpx);
+- if (winy + win.ch >= borderpx + win.th)
++ xclear(winx, 0, winx + width, win.vborderpx);
++ if (winy + win.ch >= win.vborderpx + win.th)
+ xclear(winx, winy + win.ch, winx + width, win.h);
+
+ /* Clean up the region we want to draw to. */
+@@ -1491,18 +1632,68 @@
+ r.width = width;
+ XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1);
+
+- /* Render the glyphs. */
+- XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
++ /* Decoration color. */
++ Color decor;
++ uint32_t decorcolor = tgetdecorcolor(&base);
++ if (decorcolor == DECOR_DEFAULT_COLOR) {
++ decor = *fg;
++ } else if (IS_TRUECOL(decorcolor)) {
++ colfg.alpha = 0xffff;
++ colfg.red = TRUERED(decorcolor);
++ colfg.green = TRUEGREEN(decorcolor);
++ colfg.blue = TRUEBLUE(decorcolor);
++ XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor);
++ } else {
++ decor = dc.col[decorcolor];
++ }
++ decor.color.alpha = 0xffff;
++ decor.pixel |= 0xff << 24;
+
+- /* Render underline and strikethrough. */
++ /* Float thickness, used as a base to compute other values. */
++ float fthick = dc.font.height / 18.0;
++ /* Integer thickness in pixels. Must not be 0. */
++ int thick = MAX(1, roundf(fthick));
++ /* The default gap between the baseline and a single underline. */
++ int gap = roundf(fthick * 2);
++ /* The total thickness of a double underline. */
++ int doubleh = thick * 2 + ceilf(fthick * 0.5);
++ /* The total thickness of an undercurl. */
++ int curlh = thick * 2 + roundf(fthick * 0.75);
++
++ /* Render the underline before the glyphs. */
+ if (base.mode & ATTR_UNDERLINE) {
+- XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1,
+- width, 1);
++ uint32_t style = tgetdecorstyle(&base);
++ int liney = winy + dc.font.ascent + gap;
++ /* Adjust liney to guarantee that a single underline fits. */
++ liney -= MAX(0, liney + thick - (winy + win.ch));
++ if (style == UNDERLINE_DOUBLE) {
++ liney -= MAX(0, liney + doubleh - (winy + win.ch));
++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick);
++ XftDrawRect(xw.draw, &decor, winx,
++ liney + doubleh - thick, width, thick);
++ } else if (style == UNDERLINE_DOTTED) {
++ xdrawunderdashed(xw.draw, &decor, winx, liney, width,
++ thick * 2, 0.5, thick);
++ } else if (style == UNDERLINE_DASHED) {
++ int wavelen = MAX(2, win.cw * 0.9);
++ xdrawunderdashed(xw.draw, &decor, winx, liney, width,
++ wavelen, 0.65, thick);
++ } else if (style == UNDERLINE_CURLY) {
++ liney -= MAX(0, liney + curlh - (winy + win.ch));
++ xdrawundercurl(xw.draw, &decor, winx, liney, width,
++ curlh, thick);
++ } else {
++ XftDrawRect(xw.draw, &decor, winx, liney, width, thick);
++ }
+ }
+
++ /* Render the glyphs. */
++ XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
++
++ /* Render strikethrough. Alway use the fg color. */
+ if (base.mode & ATTR_STRUCK) {
+- XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3,
+- width, 1);
++ XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3,
++ width, thick);
+ }
+
+ /* Reset clip to none. */
+@@ -1517,6 +1708,11 @@
+
+ numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y);
+ xdrawglyphfontspecs(&spec, g, numspecs, x, y);
++ if (g.mode & ATTR_IMAGE) {
++ gr_start_drawing(xw.buf, win.cw, win.ch);
++ xdrawoneimagecell(g, x, y);
++ gr_finish_drawing(xw.buf);
++ }
+ }
+
+ void
+@@ -1532,6 +1728,10 @@
+ if (IS_SET(MODE_HIDE))
+ return;
+
++ // If it's an image, just draw a ballot box for simplicity.
++ if (g.mode & ATTR_IMAGE)
++ g.u = 0x2610;
++
+ /*
+ * Select the right color for the right mode.
+ */
+@@ -1572,39 +1772,167 @@
+ case 3: /* Blinking Underline */
+ case 4: /* Steady Underline */
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + (cy + 1) * win.ch - \
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + (cy + 1) * win.ch - \
+ cursorthickness,
+ win.cw, cursorthickness);
+ break;
+ case 5: /* Blinking bar */
+ case 6: /* Steady bar */
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ cursorthickness, win.ch);
+ break;
+ }
+ } else {
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ win.cw - 1, 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ 1, win.ch - 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + (cx + 1) * win.cw - 1,
+- borderpx + cy * win.ch,
++ win.hborderpx + (cx + 1) * win.cw - 1,
++ win.vborderpx + cy * win.ch,
+ 1, win.ch - 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + (cy + 1) * win.ch - 1,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + (cy + 1) * win.ch - 1,
+ win.cw, 1);
+ }
+ }
+
++/* Draw (or queue for drawing) image cells between columns x1 and x2 assuming
++ * that they have the same attributes (and thus the same lower 24 bits of the
++ * image ID and the same placement ID). */
++void
++xdrawimages(Glyph base, Line line, int x1, int y1, int x2) {
++ int y_pix = win.vborderpx + y1 * win.ch;
++ uint32_t image_id_24bits = base.fg & 0xFFFFFF;
++ uint32_t placement_id = tgetimgplacementid(&base);
++ // Columns and rows are 1-based, 0 means unspecified.
++ int last_col = 0;
++ int last_row = 0;
++ int last_start_col = 0;
++ int last_start_x = x1;
++ // The most significant byte is also 1-base, subtract 1 before use.
++ uint32_t last_id_4thbyteplus1 = 0;
++ // We may need to inherit row/column/4th byte from the previous cell.
++ Glyph *prev = &line[x1 - 1];
++ if (x1 > 0 && (prev->mode & ATTR_IMAGE) &&
++ (prev->fg & 0xFFFFFF) == image_id_24bits &&
++ prev->decor == base.decor) {
++ last_row = tgetimgrow(prev);
++ last_col = tgetimgcol(prev);
++ last_id_4thbyteplus1 = tgetimgid4thbyteplus1(prev);
++ last_start_col = last_col + 1;
++ }
++ for (int x = x1; x < x2; ++x) {
++ Glyph *g = &line[x];
++ uint32_t cur_row = tgetimgrow(g);
++ uint32_t cur_col = tgetimgcol(g);
++ uint32_t cur_id_4thbyteplus1 = tgetimgid4thbyteplus1(g);
++ uint32_t num_diacritics = tgetimgdiacriticcount(g);
++ // If the row is not specified, assume it's the same as the row
++ // of the previous cell. Note that `cur_row` may contain a
++ // value imputed earlier, which will be preserved if `last_row`
++ // is zero (i.e. we don't know the row of the previous cell).
++ if (last_row && (num_diacritics == 0 || !cur_row))
++ cur_row = last_row;
++ // If the column is not specified and the row is the same as the
++ // row of the previous cell, then assume that the column is the
++ // next one.
++ if (last_col && (num_diacritics <= 1 || !cur_col) &&
++ cur_row == last_row)
++ cur_col = last_col + 1;
++ // If the additional id byte is not specified and the
++ // coordinates are consecutive, assume the byte is also the
++ // same.
++ if (last_id_4thbyteplus1 &&
++ (num_diacritics <= 2 || !cur_id_4thbyteplus1) &&
++ cur_row == last_row && cur_col == last_col + 1)
++ cur_id_4thbyteplus1 = last_id_4thbyteplus1;
++ // If we couldn't infer row and column, start from the top left
++ // corner.
++ if (cur_row == 0)
++ cur_row = 1;
++ if (cur_col == 0)
++ cur_col = 1;
++ // If this cell breaks a contiguous stripe of image cells, draw
++ // that line and start a new one.
++ if (cur_col != last_col + 1 || cur_row != last_row ||
++ cur_id_4thbyteplus1 != last_id_4thbyteplus1) {
++ uint32_t image_id = image_id_24bits;
++ if (last_id_4thbyteplus1)
++ image_id |= (last_id_4thbyteplus1 - 1) << 24;
++ if (last_row != 0) {
++ int x_pix =
++ win.hborderpx + last_start_x * win.cw;
++ gr_append_imagerect(
++ xw.buf, image_id, placement_id,
++ last_start_col - 1, last_col,
++ last_row - 1, last_row, last_start_x,
++ y1, x_pix, y_pix, win.cw, win.ch,
++ base.mode & ATTR_REVERSE);
++ }
++ last_start_col = cur_col;
++ last_start_x = x;
++ }
++ last_row = cur_row;
++ last_col = cur_col;
++ last_id_4thbyteplus1 = cur_id_4thbyteplus1;
++ // Populate the missing glyph data to enable inheritance between
++ // runs and support the naive implementation of tgetimgid.
++ if (!tgetimgrow(g))
++ tsetimgrow(g, cur_row);
++ // We cannot save this information if there are > 511 cols.
++ if (!tgetimgcol(g) && (cur_col & ~0x1ff) == 0)
++ tsetimgcol(g, cur_col);
++ if (!tgetimgid4thbyteplus1(g))
++ tsetimg4thbyteplus1(g, cur_id_4thbyteplus1);
++ }
++ uint32_t image_id = image_id_24bits;
++ if (last_id_4thbyteplus1)
++ image_id |= (last_id_4thbyteplus1 - 1) << 24;
++ // Draw the last contiguous stripe.
++ if (last_row != 0) {
++ int x_pix = win.hborderpx + last_start_x * win.cw;
++ gr_append_imagerect(xw.buf, image_id, placement_id,
++ last_start_col - 1, last_col, last_row - 1,
++ last_row, last_start_x, y1, x_pix, y_pix,
++ win.cw, win.ch, base.mode & ATTR_REVERSE);
++ }
++}
++
++/* Draw just one image cell without inheriting attributes from the left. */
++void xdrawoneimagecell(Glyph g, int x, int y) {
++ if (!(g.mode & ATTR_IMAGE))
++ return;
++ int x_pix = win.hborderpx + x * win.cw;
++ int y_pix = win.vborderpx + y * win.ch;
++ uint32_t row = tgetimgrow(&g) - 1;
++ uint32_t col = tgetimgcol(&g) - 1;
++ uint32_t placement_id = tgetimgplacementid(&g);
++ uint32_t image_id = tgetimgid(&g);
++ gr_append_imagerect(xw.buf, image_id, placement_id, col, col + 1, row,
++ row + 1, x, y, x_pix, y_pix, win.cw, win.ch,
++ g.mode & ATTR_REVERSE);
++}
++
++/* Prepare for image drawing. */
++void xstartimagedraw(int *dirty, int rows) {
++ gr_start_drawing(xw.buf, win.cw, win.ch);
++ gr_mark_dirty_animations(dirty, rows);
++}
++
++/* Draw all queued image cells. */
++void xfinishimagedraw() {
++ gr_finish_drawing(xw.buf);
++}
++
+ void
+ xsetenv(void)
+ {
+@@ -1671,6 +1999,8 @@
+ new.mode ^= ATTR_REVERSE;
+ if (i > 0 && ATTRCMP(base, new)) {
+ xdrawglyphfontspecs(specs, base, i, ox, y1);
++ if (base.mode & ATTR_IMAGE)
++ xdrawimages(base, line, ox, y1, x);
+ specs += i;
+ numspecs -= i;
+ i = 0;
+@@ -1683,6 +2013,8 @@
+ }
+ if (i > 0)
+ xdrawglyphfontspecs(specs, base, i, ox, y1);
++ if (i > 0 && base.mode & ATTR_IMAGE)
++ xdrawimages(base, line, ox, y1, x);
+ }
+
+ void
+@@ -1907,6 +2239,7 @@
+ }
+ } else if (e->xclient.data.l[0] == xw.wmdeletewin) {
+ ttyhangup();
++ gr_deinit();
+ exit(0);
+ }
+ }
+@@ -1957,6 +2290,13 @@
+ if (XPending(xw.dpy))
+ timeout = 0; /* existing events might not set xfd */
+
++ /* Decrease the timeout if there are active animations. */
++ if (graphics_next_redraw_delay != INT_MAX &&
++ IS_SET(MODE_VISIBLE))
++ timeout = timeout < 0 ? graphics_next_redraw_delay
++ : MIN(timeout,
++ graphics_next_redraw_delay);
++
+ seltv.tv_sec = timeout / 1E3;
+ seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec);
+ tv = timeout >= 0 ? &seltv : NULL;
+@@ -2023,6 +2363,118 @@
+ }
+ }
+
++
++#define XRESOURCE_LOAD_META(NAME) \
++ if(!XrmGetResource(xrdb, "st." NAME, "st." NAME, &type, &ret)) \
++ XrmGetResource(xrdb, "*." NAME, "*." NAME, &type, &ret); \
++ if (ret.addr != NULL && !strncmp("String", type, 64))
++
++#define XRESOURCE_LOAD_STRING(NAME, DST) \
++ XRESOURCE_LOAD_META(NAME) \
++ DST = ret.addr;
++
++#define XRESOURCE_LOAD_CHAR(NAME, DST) \
++ XRESOURCE_LOAD_META(NAME) \
++ DST = ret.addr[0];
++
++#define XRESOURCE_LOAD_INTEGER(NAME, DST) \
++ XRESOURCE_LOAD_META(NAME) \
++ DST = strtoul(ret.addr, NULL, 10);
++
++#define XRESOURCE_LOAD_FLOAT(NAME, DST) \
++ XRESOURCE_LOAD_META(NAME) \
++ DST = strtof(ret.addr, NULL);
++
++void
++xrdb_load(void)
++{
++ /* XXX */
++ char *xrm;
++ char *type;
++ XrmDatabase xrdb;
++ XrmValue ret;
++ Display *dpy;
++
++ if(!(dpy = XOpenDisplay(NULL)))
++ die("Can't open display\n");
++
++ XrmInitialize();
++ xrm = XResourceManagerString(dpy);
++
++ if (xrm != NULL) {
++ xrdb = XrmGetStringDatabase(xrm);
++
++ /* handling colors here without macros to do via loop. */
++ int i = 0;
++ char loadValue[12] = "";
++ for (i = 0; i < 256; i++)
++ {
++ sprintf(loadValue, "%s%d", "st.color", i);
++
++ if(!XrmGetResource(xrdb, loadValue, loadValue, &type, &ret))
++ {
++ sprintf(loadValue, "%s%d", "*.color", i);
++ if (!XrmGetResource(xrdb, loadValue, loadValue, &type, &ret))
++ /* reset if not found (unless in range for defaults). */
++ if (i > 15)
++ colorname[i] = NULL;
++ }
++
++ if (ret.addr != NULL && !strncmp("String", type, 64))
++ colorname[i] = ret.addr;
++ }
++
++ XRESOURCE_LOAD_STRING("foreground", colorname[defaultfg]);
++ XRESOURCE_LOAD_STRING("background", colorname[defaultbg]);
++ XRESOURCE_LOAD_STRING("cursorColor", colorname[defaultcs])
++ else {
++ // this looks confusing because we are chaining off of the if
++ // in the macro. probably we should be wrapping everything blocks
++ // so this isn't possible...
++ defaultcs = defaultfg;
++ }
++ XRESOURCE_LOAD_STRING("reverse-cursor", colorname[defaultrcs])
++ else {
++ // see above.
++ defaultrcs = defaultbg;
++ }
++
++ XRESOURCE_LOAD_STRING("font", font);
++ XRESOURCE_LOAD_STRING("termname", termname);
++
++ XRESOURCE_LOAD_INTEGER("blinktimeout", blinktimeout);
++ XRESOURCE_LOAD_INTEGER("bellvolume", bellvolume);
++ XRESOURCE_LOAD_INTEGER("borderpx", borderpx);
++ XRESOURCE_LOAD_INTEGER("cursorshape", cursorshape);
++
++ XRESOURCE_LOAD_FLOAT("cwscale", cwscale);
++ XRESOURCE_LOAD_FLOAT("chscale", chscale);
++ }
++ XFlush(dpy);
++}
++
++void
++reload(int sig)
++{
++ xrdb_load();
++
++ /* colors, fonts */
++ xloadcols();
++ xunloadfonts();
++ xloadfonts(font, 0);
++
++ /* pretend the window just got resized */
++ cresize(win.w, win.h);
++
++ redraw();
++
++ /* triggers re-render if we're visible. */
++ ttywrite("\033[O", 3, 1);
++
++ signal(SIGUSR1, reload);
++}
++
++
+ void
+ usage(void)
+ {
+@@ -2096,6 +2548,8 @@
+
+ setlocale(LC_CTYPE, "");
+ XSetLocaleModifiers("");
++ xrdb_load();
++ signal(SIGUSR1, reload);
+ cols = MAX(cols, 1);
+ rows = MAX(rows, 1);
+ tnew(cols, rows);