From 3a4e096d9b2a4a928c55f0b24b87a00b3621269b Mon Sep 17 00:00:00 2001 From: a Date: Sun, 6 Sep 2026 22:36:33 -0500 Subject: [PATCH] Terminal compatibility batch - Answer kitty keyboard protocol queries (CSI ?u -> CSI ?0u) and silently accept push/pop/set so clients detect the lack of support without a timeout, and so st stops logging them as unknown sequences. - XTVERSION (CSI >0q) reports st(VERSION). - DECRQM (CSI ?Ps$p / CSI Ps$p) reports the state of the common private and ANSI modes including 2026 (synchronized updates) and 2004. - csiparse() now recognises the >, < and = private prefixes. - New OSC7_OSC8_PATCH: OSC 7 stores the shell's working directory in term.cwd; OSC 8 hyperlinks are stored per cell and, with the openurlonclick patch, are clickable and take precedence over URL text detection. - kitty: Unicode placeholder cells are now blitted from one cached scaled pixmap per virtual placement instead of rescaling every cell on every redraw. --- kitty.c | 120 ++++------------------ patch/openurlonclick.c | 40 ++++++++ patch/reflow.c | 3 + patches.def.h | 8 ++ patches.h | 8 ++ st.c | 226 ++++++++++++++++++++++++++++++++++++++++- st.h | 11 ++ 7 files changed, 317 insertions(+), 99 deletions(-) diff --git a/kitty.c b/kitty.c index 11de61b..74d2457 100644 --- a/kitty.c +++ b/kitty.c @@ -2295,122 +2295,46 @@ kitty_find_virtual(uint32_t imgid, uint32_t plcid) /* * 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. + * + * The whole placement is scaled once into the placement's pixmap cache + * (kitty_prepare), and each cell is then a cheap XRender copy out of it. */ 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; + int w, h, bx, by, cw = win.cw, ch = win.ch; + int sx, sy, cellw, cellh; - if (!im || row >= p->rows || col >= p->cols) + if (!p->image || row >= p->rows || col >= p->cols) + return; + if (!kitty_prepare(p, &w, &h)) return; - fullw = p->cols * cw; - fullh = p->rows * ch; - if (fullw <= 0 || fullh <= 0) + sx = col * cw; + sy = row * ch; + cellw = KMIN(cw, w - sx); + cellh = KMIN(ch, h - sy); + if (cellw <= 0 || cellh <= 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) + return; + src = XRenderCreatePicture(xw.dpy, (Drawable)p->pixmap, fmt, 0, &pa); + fmt = XRenderFindVisualFormat(xw.dpy, xw.vis); 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); + dstpic = XRenderCreatePicture(xw.dpy, xw.buf, fmt, 0, &pa); + XRenderComposite(xw.dpy, PictOpOver, src, None, dstpic, sx, sy, 0, 0, + bx + px * cw, by + py * ch, cellw, cellh); + XRenderFreePicture(xw.dpy, dstpic); } - XFreePixmap(xw.dpy, pm); + XRenderFreePicture(xw.dpy, src); } /* scan the visible screen for placeholder glyphs and draw their images */ diff --git a/patch/openurlonclick.c b/patch/openurlonclick.c index 54b1a02..5cf5d56 100644 --- a/patch/openurlonclick.c +++ b/patch/openurlonclick.c @@ -61,6 +61,30 @@ clearurl(void) url_y2 = -1; } +#if OSC7_OSC8_PATCH +/* an explicit OSC 8 hyperlink takes precedence over text detection */ +static char * +detecthyperlink(int col, int row, int draw) +{ + int x1, y1, x2, y2; + uint32_t id = hyperlink_at(col, row, &x1, &y1, &x2, &y2); + const char *url; + + if (!id || !(url = hyperlink_get(id))) + return NULL; + if (draw) { + url_x1 = x1; + url_x2 = x2; + url_y1 = y1; + url_y2 = y2; + url_maxcol = x2; + url_draw = 1; + term.dirty[y1] = 1; + } + return (char *)url; +} +#endif // OSC7_OSC8_PATCH + #if REFLOW_PATCH char * detecturl(int col, int row, int draw) @@ -72,11 +96,19 @@ detecturl(int col, int row, int draw) int row_start = row, col_start = col; int minrow = tisaltscr() ? 0 : term.scr - term.histf; int maxrow = tisaltscr() ? term.row - 1 : term.scr + term.row - 1; + #if OSC7_OSC8_PATCH + char *link; + #endif // OSC7_OSC8_PATCH /* clear previously underlined url */ if (draw) clearurl(); + #if OSC7_OSC8_PATCH + if ((link = detecthyperlink(col, row, draw))) + return link; + #endif // OSC7_OSC8_PATCH + url_maxcol = 0; line = TLINE(row); @@ -143,6 +175,9 @@ char * detecturl(int col, int row, int draw) { static char url[2048]; + #if OSC7_OSC8_PATCH + char *link; + #endif // OSC7_OSC8_PATCH int x1, y1, x2, y2, wrapped; int row_start = row; int col_start = col; @@ -162,6 +197,11 @@ detecturl(int col, int row, int draw) if (draw) clearurl(); + #if OSC7_OSC8_PATCH + if ((link = detecthyperlink(col, row, draw))) + return link; + #endif // OSC7_OSC8_PATCH + if (!isvalidurlchar(TLINEURL(row)[col].u)) return NULL; diff --git a/patch/reflow.c b/patch/reflow.c index c95bc82..bcf0ebd 100644 --- a/patch/reflow.c +++ b/patch/reflow.c @@ -68,6 +68,9 @@ tclearglyph(Glyph *gp, int usecurattr) #if KITTY_GRAPHICS_PATCH gp->kitty = 0; #endif // KITTY_GRAPHICS_PATCH + #if OSC7_OSC8_PATCH + gp->link = 0; + #endif // OSC7_OSC8_PATCH } #if SIXEL_PATCH diff --git a/patches.def.h b/patches.def.h index 1e4edd6..83cb207 100644 --- a/patches.def.h +++ b/patches.def.h @@ -357,6 +357,14 @@ */ #define KITTY_GRAPHICS_PATCH 0 +/* This patch adds support for OSC 7 (current working directory reporting) and + * OSC 8 (hyperlinks). Shells and programs can mark ranges of text as links to + * arbitrary URLs; with the openurlonclick patch these become clickable and + * take precedence over URL text detection. The working directory reported by + * the shell is stored in term.cwd for use by e.g. new window shortcuts. + */ +#define OSC7_OSC8_PATCH 0 + /* This patch adds SIXEL graphics support for st. * Note that patch/sixel.c/sixel_hls.c come from mintty, licensed under GPL. * Known issues: diff --git a/patches.h b/patches.h index 135f733..b21f944 100644 --- a/patches.h +++ b/patches.h @@ -357,6 +357,14 @@ */ #define KITTY_GRAPHICS_PATCH 1 +/* This patch adds support for OSC 7 (current working directory reporting) and + * OSC 8 (hyperlinks). Shells and programs can mark ranges of text as links to + * arbitrary URLs; with the openurlonclick patch these become clickable and + * take precedence over URL text detection. The working directory reported by + * the shell is stored in term.cwd for use by e.g. new window shortcuts. + */ +#define OSC7_OSC8_PATCH 1 + /* This patch adds SIXEL graphics support for st. * Note that patch/sixel.c/sixel_hls.c come from mintty, licensed under GPL. * Known issues: diff --git a/st.c b/st.c index 8694e60..d6f0e7f 100644 --- a/st.c +++ b/st.c @@ -143,6 +143,7 @@ typedef struct { char buf[ESC_BUF_SIZ]; /* raw string */ size_t len; /* raw string length */ char priv; + char prefix; /* '>', '<' or '=' private prefix */ int arg[ESC_ARG_SIZ]; int narg; /* nb of args */ char mode[2]; @@ -226,6 +227,7 @@ static void tsetdirt(int, int); static void tsetscroll(int, int); static void tswapscreen(void); static void tsetmode(int, int, const int *, int); +static int tmodestatus(int, int); static int twrite(const char *, int, int); static void tcontrolcode(uchar ); static void tdectest(char ); @@ -270,6 +272,115 @@ static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF #include "patch/st_include.h" +#if OSC7_OSC8_PATCH +/* + * OSC 8 hyperlinks. Each distinct (id, url) pair gets a numeric handle that + * is stored in Glyph.link. Links with an explicit id= parameter are shared + * between all cells using that id, so a link split across lines still counts + * as one link. + */ +typedef struct { + char *id; /* the id= parameter, or NULL */ + char *url; +} Hyperlink; + +static Hyperlink *hyperlinks; /* index 0 is unused (0 == no link) */ +static uint32_t nhyperlinks, hyperlinkcap; +static uint32_t curlink; /* link applied to newly printed cells */ + +static uint32_t +hyperlink_add(const char *id, const char *url) +{ + uint32_t i; + Hyperlink *h; + + if (!url || !*url) + return 0; + + /* reuse an existing entry for the same id and url */ + for (i = 1; i < nhyperlinks; i++) { + h = &hyperlinks[i]; + if (!strcmp(h->url, url) && + ((!id && !h->id) || (id && h->id && !strcmp(id, h->id)))) + return i; + } + + if (nhyperlinks + 1 > hyperlinkcap) { + uint32_t ncap = hyperlinkcap ? hyperlinkcap * 2 : 64; + hyperlinks = xrealloc(hyperlinks, ncap * sizeof *hyperlinks); + if (!nhyperlinks) { + hyperlinks[0].id = hyperlinks[0].url = NULL; + nhyperlinks = 1; + } + hyperlinkcap = ncap; + } + h = &hyperlinks[nhyperlinks]; + h->id = (id && *id) ? xstrdup(id) : NULL; + h->url = xstrdup(url); + return nhyperlinks++; +} + +const char * +hyperlink_get(uint32_t id) +{ + if (id == 0 || id >= nhyperlinks) + return NULL; + return hyperlinks[id].url; +} + +/* handle "OSC 8 ; params ; url": params is a ':'-separated key=value list */ +static void +hyperlink_osc(const char *params, const char *url) +{ + char idbuf[256], *id = NULL; + const char *p = params; + + while (p && *p) { + const char *end = strchr(p, ':'); + size_t n = end ? (size_t)(end - p) : strlen(p); + if (n > 3 && !strncmp(p, "id=", 3)) { + n = MIN(n - 3, sizeof idbuf - 1); + memcpy(idbuf, p + 3, n); + idbuf[n] = '\0'; + id = idbuf; + } + p = end ? end + 1 : NULL; + } + curlink = hyperlink_add(id, url); +} + +/* + * Find the extent of the link at (col, row) on the visible screen. Returns + * the link handle, or 0 if the cell is not part of a link. The returned + * rectangle covers the contiguous run on this row; use hyperlink_get() for + * the url. + */ +uint32_t +hyperlink_at(int col, int row, int *x1, int *y1, int *x2, int *y2) +{ + Line line; + uint32_t id; + int x; + + if (row < 0 || row >= term.row || col < 0 || col >= term.col) + return 0; + #if REFLOW_PATCH || SCROLLBACK_PATCH + line = TLINE(row); + #else + line = term.line[row]; + #endif + if (!(id = line[col].link)) + return 0; + for (x = col; x > 0 && line[x-1].link == id; x--); + *x1 = x; + for (x = col; x < term.col-1 && line[x+1].link == id; x++); + *x2 = x; + *y1 = *y2 = row; + return id; +} +#endif // OSC7_OSC8_PATCH + + ssize_t xwrite(int fd, const char *s, size_t len) { @@ -1204,6 +1315,9 @@ treset(void) term.top = 0; term.bot = term.row - 1; term.mode = MODE_WRAP|MODE_UTF8; + #if OSC7_OSC8_PATCH + curlink = 0; + #endif // OSC7_OSC8_PATCH memset(term.trantbl, CS_USA, sizeof(term.trantbl)); term.charset = 0; #if REFLOW_PATCH @@ -1508,9 +1622,13 @@ csiparse(void) int sep = ';'; /* colon or semi-colon, but not both */ csiescseq.narg = 0; + csiescseq.prefix = '\0'; if (*p == '?') { csiescseq.priv = 1; p++; + } else if (*p == '>' || *p == '<' || *p == '=') { + csiescseq.prefix = *p; + p++; } csiescseq.buf[csiescseq.len] = '\0'; @@ -1610,6 +1728,10 @@ tsetchar(Rune u, const Glyph *attr, int x, int y) term.line[y][x].mode |= ATTR_BOXDRAW; #endif // BOXDRAW_PATCH + #if OSC7_OSC8_PATCH + term.line[y][x].link = curlink; + #endif // OSC7_OSC8_PATCH + #if KITTY_GRAPHICS_PATCH term.line[y][x].kitty = 0; if (u == KITTY_PLACEHOLDER) @@ -1951,6 +2073,49 @@ tsetscroll(int t, int b) term.bot = b; } +/* DECRQM: report the state of a mode */ +static int +tmodestatus(int priv, int mode) +{ + int wm = win.mode; + + if (priv) { + switch (mode) { + case 1: return (wm & MODE_APPCURSOR) ? 1 : 2; + case 6: return (term.c.state & CURSOR_ORIGIN) ? 1 : 2; + case 7: return IS_SET(MODE_WRAP) ? 1 : 2; + case 12: return 0; + case 25: return (wm & MODE_HIDE) ? 2 : 1; + case 47: + case 1047: + case 1049: return IS_SET(MODE_ALTSCREEN) ? 1 : 2; + case 9: return (wm & MODE_MOUSEX10) ? 1 : 2; + case 1000: return (wm & MODE_MOUSEBTN) ? 1 : 2; + case 1002: return (wm & MODE_MOUSEMOTION) ? 1 : 2; + case 1003: return (wm & MODE_MOUSEMANY) ? 1 : 2; + case 1004: return (wm & MODE_FOCUS) ? 1 : 2; + case 1006: return (wm & MODE_MOUSESGR) ? 1 : 2; + case 1034: return (wm & MODE_8BIT) ? 1 : 2; + case 2004: return (wm & MODE_BRCKTPASTE) ? 1 : 2; + #if SYNC_PATCH + case 2026: return su ? 1 : 2; + #endif // SYNC_PATCH + #if SIXEL_PATCH + case 80: return IS_SET(MODE_SIXEL_SDM) ? 1 : 2; + case 8452: return IS_SET(MODE_SIXEL_CUR_RT) ? 1 : 2; + #endif // SIXEL_PATCH + } + return 0; + } + switch (mode) { + case 2: return 4; /* KAM, always reset */ + case 4: return IS_SET(MODE_INSERT) ? 1 : 2; + case 12: return IS_SET(MODE_ECHO) ? 2 : 1; + case 20: return IS_SET(MODE_CRLF) ? 1 : 2; + } + return 0; +} + void tsetmode(int priv, int set, const int *args, int narg) { @@ -2535,9 +2700,47 @@ csihandle(void) } break; #endif // CSI_22_23_PATCH | SIXEL_PATCH - case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ + case 'u': + if (csiescseq.priv) { + /* CSI ? u -- kitty keyboard protocol: query flags. We do not + * implement the protocol, so answer with no flags set. */ + ttywrite("\033[?0u", 5, 0); + break; + } + if (csiescseq.prefix == '>' || csiescseq.prefix == '<' || + csiescseq.prefix == '=') { + /* CSI > flags u (push), CSI < flags u (pop), CSI = flags ; mode u + * (set): kitty keyboard protocol, silently ignored. */ + break; + } + /* DECRC -- Restore cursor position (ANSI.SYS) */ tcursor(CURSOR_LOAD); break; + case 'q': + if (csiescseq.prefix == '>') { + /* XTVERSION -- CSI > Ps q: report terminal name and version */ + n = snprintf(buffer, sizeof(buffer), "\033P>|st(%s)\033\\", VERSION); + ttywrite(buffer, n, 0); + break; + } + goto unknown; + case '$': + switch (csiescseq.mode[1]) { + case 'p': /* DECRQM -- request mode */ + /* + * Reply: CSI ? Pd ; Ps $ y (ANSI: CSI Pd ; Ps $ y) + * Ps: 0 not recognized, 1 set, 2 reset, + * 3 permanently set, 4 permanently reset + */ + n = snprintf(buffer, sizeof(buffer), "\033[%s%d;%d$y", + csiescseq.priv ? "?" : "", csiescseq.arg[0], + tmodestatus(csiescseq.priv, csiescseq.arg[0])); + ttywrite(buffer, n, 0); + break; + default: + goto unknown; + } + break; case ' ': switch (csiescseq.mode[1]) { case 'q': /* DECSCUSR -- Set Cursor Style */ @@ -2678,8 +2881,29 @@ strhandle(void) } } return; + #if OSC7_OSC8_PATCH + case 7: /* current working directory: file://host/path */ + if (narg > 1) { + p = strescseq.args[1]; + if (!strncmp(p, "file://", 7)) { + /* skip the host part */ + p += 7; + p = strchr(p, '/'); + } + free(term.cwd); + term.cwd = (p && *p == '/') ? xstrdup(p) : NULL; + } + return; + case 8: /* hyperlink: OSC 8 ; params ; url */ + if (narg > 2 && strescseq.args[2][0]) + hyperlink_osc(strescseq.args[1], strescseq.args[2]); + else + curlink = 0; + return; + #else case 8: /* Clear Hyperlinks */ return; + #endif // OSC7_OSC8_PATCH case 10: case 11: case 12: diff --git a/st.h b/st.h index 32012ed..437498b 100644 --- a/st.h +++ b/st.h @@ -150,6 +150,9 @@ typedef struct { #if KITTY_GRAPHICS_PATCH uint32_t kitty; /* kitty Unicode placeholder row/column/msb */ #endif // KITTY_GRAPHICS_PATCH + #if OSC7_OSC8_PATCH + uint32_t link; /* OSC 8 hyperlink id, 0 == none */ + #endif // OSC7_OSC8_PATCH } Glyph; typedef Glyph *Line; @@ -208,6 +211,9 @@ typedef struct { ImageList *images_alt; /* sixel images for alternate screen */ #endif // SIXEL_PATCH Rune lastc; /* last printed char outside of sequence, 0 if control */ + #if OSC7_OSC8_PATCH + char *cwd; /* working directory reported via OSC 7, or NULL */ + #endif // OSC7_OSC8_PATCH } Term; typedef union { @@ -376,6 +382,11 @@ char *getsel(void); size_t utf8encode(Rune, char *); +#if OSC7_OSC8_PATCH +const char *hyperlink_get(uint32_t id); +uint32_t hyperlink_at(int col, int row, int *x1, int *y1, int *x2, int *y2); +#endif // OSC7_OSC8_PATCH + void *xmalloc(size_t); void *xrealloc(void *, size_t); char *xstrdup(const char *);