[hackers] [PATCH v2] Implement Kitty underline color sequences

From: Roberto E. Vargas Caballero <k0ga_AT_shike2.net>
Date: Tue, 30 Dec 2025 22:04:13 +0100

St was faking the set underline color sequence from the
Kitty [1] terminal. This patch implements correctly the
sequences SGR 58 and SGR 59 to enable or disable chaging
the color.

[1] https://sw.kovidgoyal.net/kitty/underlines/
---
 st.c    | 15 ++++++++++-----
 st.h    |  2 ++
 st.info |  6 ++++++
 x.c     | 20 +++++++++++++++++---
 4 files changed, 35 insertions(+), 8 deletions(-)
diff --git a/st.c b/st.c
index e55e7b3..4fd230d 100644
--- a/st.c
+++ b/st.c
_AT_@ -1015,7 +1015,8 @@ treset(void)
 	term.c = (TCursor){{
 		.mode = ATTR_NULL,
 		.fg = defaultfg,
-		.bg = defaultbg
+		.bg = defaultbg,
+		.un = defaultfg,
 	}, .x = 0, .y = 0, .state = CURSOR_DEFAULT};
 
 	memset(term.tabs, 0, term.col * sizeof(*term.tabs));
_AT_@ -1362,6 +1363,7 @@ tsetattr(const int *attr, int l)
 				ATTR_FAINT      |
 				ATTR_ITALIC     |
 				ATTR_UNDERLINE  |
+			        ATTR_UNDERCOLOR |
 				ATTR_BLINK      |
 				ATTR_REVERSE    |
 				ATTR_INVISIBLE  |
_AT_@ -1431,10 +1433,13 @@ 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) {
+				term.c.attr.mode |= ATTR_UNDERCOLOR;
+				term.c.attr.un = idx;
+			}
+			break;
+		case 59:
+			term.c.attr.mode &= ~ATTR_UNDERCOLOR;
 			break;
 		default:
 			if (BETWEEN(attr[i], 30, 37)) {
diff --git a/st.h b/st.h
index fd3b0d8..4cc16e3 100644
--- a/st.h
+++ b/st.h
_AT_@ -33,6 +33,7 @@ enum glyph_attribute {
 	ATTR_WRAP       = 1 << 8,
 	ATTR_WIDE       = 1 << 9,
 	ATTR_WDUMMY     = 1 << 10,
+	ATTR_UNDERCOLOR = 1 << 11,
 	ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
 };
 
_AT_@ -65,6 +66,7 @@ typedef struct {
 	ushort mode;      /* attribute flags */
 	uint32_t fg;      /* foreground  */
 	uint32_t bg;      /* background  */
+	uint32_t un;      /* underline */
 } Glyph;
 
 typedef Glyph *Line;
diff --git a/st.info b/st.info
index efab2cf..803393e 100644
--- a/st.info
+++ b/st.info
_AT_@ -204,6 +204,9 @@ st| simpleterm,
 	setb=\E[4%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m,
 	setf=\E[3%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m,
 	sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m,
+# kitty extensions
+	Setulc=\E[58\:5\:%p1%dm,
+
 
 st-256color| simpleterm with 256 colors,
 	use=st,
_AT_@ -215,6 +218,9 @@ st-256color| simpleterm with 256 colors,
 	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,
+# kitty extensions
+	Setulc=\E[58\:2\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m,
+
 
 st-meta| simpleterm with meta key,
 	use=st,
diff --git a/x.c b/x.c
index d73152b..d5b1dd3 100644
--- a/x.c
+++ b/x.c
_AT_@ -1380,8 +1380,8 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
 	int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
 	int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
 	    width = charlen * win.cw;
-	Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
-	XRenderColor colfg, colbg;
+	Color *un, *fg, *bg, *temp, revfg, revbg, trueun, truefg, truebg;
+	XRenderColor colun, colfg, colbg;
 	XRectangle r;
 
 	/* Fallback on color display for attributes not supported by the font */
_AT_@ -1466,6 +1466,20 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
 	if (base.mode & ATTR_INVISIBLE)
 		fg = bg;
 
+	un = fg;
+	if (base.mode & ATTR_UNDERCOLOR) {
+		if (IS_TRUECOL(base.un)) {
+			colun.alpha = 0xffff;
+			colun.red = TRUERED(base.un);
+			colun.green = TRUEGREEN(base.un);
+			colun.blue = TRUEBLUE(base.un);
+			XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colun, &trueun);
+			un = &trueun;
+		} else {
+			un = &dc.col[base.un];
+		}
+	}
+
 	/* Intelligent cleaning up of the borders. */
 	if (x == 0) {
 		xclear(0, (y == 0)? 0 : winy, borderpx,
_AT_@ -1496,7 +1510,7 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
 
 	/* Render underline and strikethrough. */
 	if (base.mode & ATTR_UNDERLINE) {
-		XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1,
+		XftDrawRect(xw.draw, un, winx, winy + dc.font.ascent * chscale + 1,
 				width, 1);
 	}
 
-- 
2.46.1
Received on Tue Dec 30 2025 - 22:04:13 CET

This archive was generated by hypermail 2.3.0 : Tue Dec 30 2025 - 22:12:32 CET