Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: Terminal theme tool – a workaround for lack of 24-bit color in Zsh?



On 15 Oct, Sebastian Gniazdowski wrote:
> There are 2 escape sequences, ESC]4;n;#rrggbb\a and ESC]Pnrrggbb, that
> allow to change terminal's current palette. First works on most

> I'm curious on opinions whether this really is a workaround for no
> true-color support in Zsh and maybe on ideas of what such
> terminal-theme manager should do.

That does cause the colours further back in the scrollback buffer
to change. I find 256 colours to be plenty for my needs but it would
be nice to be able to specify colours consistently such as with hex
triples. I still need to deal with 88-colour terminals and that
makes it harder to setup consistent themes. For themes, agreeing
on a particular associative array to map tokens to colours may work
well enough. Perhaps with explicit support in zsh where colours are
specified.

The patch below takes a different approach to this. Colours specified as
hex triples are matched to the nearest colour in the usual 256-colour
palette.

The patch adds a module with a hook function for retrieving the colour.
Actual true colour support wouldn't fit well in a module as it would
mostly affect the existing attribute tracking code. But this module can
act as a substitute while allowing a consistent way to specify colours.

True colour would require upping the attributes to 64-bits to make
space for 24-bits for the colour. To do that, the same questions I
posed for PM_ flags would apply. Can we just use uint64_t or is
autoconf magic required? A second int is also possible but somewhat
ugly because it means an extra parameter to some functions.

An alternative that keeps the attributes within 32-bits would be to
keep our own palette of 256 custom colours. Sebastian's approach of
redefining the terminal's palette is also an option.

The nearest colour matching code converts RGB colours to CIE L*a*b*.
There are more sophisticated methods but this does a noticably better
job than just taking distances in RGB space. It doesn't use any of the
first 16 colours, those are regularly configured and can't be relied
upon. While it is possible that someone has the whole range configured,
I don't think that's very common and, if really needed, there is an
escape sequence to get the colour values. It does compare directly
against all possible colours. I experimented with trying to optimise
it to avoid that but comparing 240 appears instant, even on my
decade old PC, and alternatives just ended up with longer code.

I'd be interested if anyone has any thoughts or views on any of this?

Oliver

PS. Thanks to Sebastian for testing out an earlier version of the
colour matching code.

diff --git a/Src/Modules/nearcolor.c b/Src/Modules/nearcolor.c
new file mode 100644
index 000000000..2a763d470
--- /dev/null
+++ b/Src/Modules/nearcolor.c
@@ -0,0 +1,178 @@
+#include "nearcolor.mdh"
+#include "nearcolor.pro"
+
+#include <math.h>
+
+struct cielab {
+    float L, a, b;
+};
+typedef struct cielab *Cielab;
+
+static float
+deltae(Cielab lab1, Cielab lab2)
+{
+    /* taking square root unnecessary as we're just comparing values */
+    return powf(lab1->L - lab2->L, 2) +
+	powf(lab1->a - lab2->a, 2) +
+	powf(lab1->b - lab2->b, 2);
+}
+
+static void
+RGBtoLAB(int red, int green, int blue, Cielab lab)
+{
+    float R = (float)red / 255.0;
+    float G = (float)green / 255.0;
+    float B = (float)blue / 255.0;
+    R = 100.0 * (R > 0.04045 ? powf((R + 0.055) / 1.055, 2.4) : R / 12.92);
+    G = 100.0 * (G > 0.04045 ? powf((G + 0.055) / 1.055, 2.4) : G / 12.92);
+    B = 100.0 * (B > 0.04045 ? powf((B + 0.055) / 1.055, 2.4) : B / 12.92);
+
+    /* Observer. = 2 degrees, Illuminant = D65 */
+    float X = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 95.047;
+    float Y = (R * 0.2126 + G * 0.7152 + B * 0.0722) / 100.0;
+    float Z = (R * 0.0193 + G * 0.1192 + B * 0.9505) / 108.883;
+
+    X = (X > 0.008856) ? powf(X, 1.0/3.0) : (7.787 * X) + (16.0 / 116.0);
+    Y = (Y > 0.008856) ? powf(Y, 1.0/3.0) : (7.787 * Y) + (16.0 / 116.0);
+    Z = (Z > 0.008856) ? powf(Z, 1.0/3.0) : (7.787 * Z) + (16.0 / 116.0);
+
+    lab->L = (116.0 * Y) - 16.0;
+    lab->a = 500.0 * (X - Y);
+    lab->b = 200.0 * (Y - Z);
+}
+
+static int
+mapRGBto88(int red, int green, int blue)
+{
+    int component[] = { 0, 0x8b, 0xcd, 0xff, 0x2e, 0x5c, 0x8b, 0xa2, 0xb9, 0xd0, 0xe7 };
+    struct cielab orig, next;
+    float nextl, bestl = -1;
+    int r, g, b;
+    int comp_r = 0, comp_g = 0, comp_b = 0;
+
+    /* Get original value */
+    RGBtoLAB(red, green, blue, &orig);
+
+    /* try every one of the 72 colours */
+    for (r = 0; r < 11; r++) {
+	for (g = 0; g <= 3; g++) {
+	    for (b = 0; b <= 3; b++) {
+		if (r > 3) g = b = r; /* advance inner loops to the block of greys */
+		RGBtoLAB(component[r], component[g], component[b], &next);
+		nextl = deltae(&orig, &next);
+		if (nextl < bestl || bestl < 0) {
+		    bestl = nextl;
+		    comp_r = r;
+		    comp_g = g;
+		    comp_b = b;
+		}
+	    }
+	}
+    }
+
+    return (comp_r > 3) ? 77 + comp_r :
+        16 + (comp_r * 16) + (comp_g * 4) + comp_b;
+}
+
+/*
+ * Convert RGB to nearest colour in the 256 colour range
+ */
+static int
+mapRGBto256(int red, int green, int blue)
+{
+    int component[] = {
+	0, 0x5f, 0x87, 0xaf, 0xd7, 0xff,
+	0x8, 0x12, 0x1c, 0x26, 0x30, 0x3a, 0x44, 0x4e,
+	0x58, 0x62, 0x6c, 0x76, 0x80, 0x8a, 0x94, 0x9e,
+	0xa8, 0xb2, 0xbc, 0xc6, 0xd0, 0xda, 0xe4, 0xee
+    };
+    struct cielab orig, next;
+    float nextl, bestl = -1;
+    int r, g, b;
+    int comp_r = 0, comp_g = 0, comp_b = 0;
+
+    /* Get original value */
+    RGBtoLAB(red, green, blue, &orig);
+
+    for (r = 0; r < sizeof(component)/sizeof(*component); r++) {
+	for (g = 0; g <= 5; g++) {
+	    for (b = 0; b <= 5; b++) {
+		if (r > 5) g = b = r; /* advance inner loops to the block of greys */
+		RGBtoLAB(component[r], component[g], component[b], &next);
+		nextl = deltae(&orig, &next);
+		if (nextl < bestl || bestl < 0) {
+		    bestl = nextl;
+		    comp_r = r;
+		    comp_g = g;
+		    comp_b = b;
+		}
+	    }
+	}
+    }
+
+    return (comp_r > 5) ? 226 + comp_r :
+	16 + (comp_r * 36) + (comp_g * 6) + comp_b;
+}
+
+static int
+getnearestcolor(UNUSED(Hookdef dummy), Color_rgb col)
+{
+    if (tccolours == 256)
+	return mapRGBto256(col->red, col->green, col->blue);
+    if (tccolours == 88)
+	return mapRGBto88(col->red, col->green, col->blue);
+    return 0;
+}
+
+static struct features module_features = {
+    NULL, 0,
+    NULL, 0,
+    NULL, 0,
+    NULL, 0,
+    0
+};
+
+/**/
+int
+setup_(UNUSED(Module m))
+{
+    return 0;
+}
+
+/**/
+int
+features_(Module m, char ***features)
+{
+    *features = featuresarray(m, &module_features);
+    return 0;
+}
+
+/**/
+int
+enables_(Module m, int **enables)
+{
+    return handlefeatures(m, &module_features, enables);
+}
+
+/**/
+int
+boot_(Module m)
+{
+    addhookfunc("get_color_attr", (Hookfn) getnearestcolor);
+    return 0;
+}
+
+/**/
+int
+cleanup_(Module m)
+{
+    deletehookfunc("get_color_attr", (Hookfn) getnearestcolor);
+    return setfeatureenables(m, &module_features, NULL);
+}
+
+/**/
+int
+finish_(UNUSED(Module m))
+{
+    return 0;
+}
diff --git a/Src/Modules/nearcolor.mdd b/Src/Modules/nearcolor.mdd
new file mode 100644
index 000000000..2fcdaf04e
--- /dev/null
+++ b/Src/Modules/nearcolor.mdd
@@ -0,0 +1,5 @@
+name=zsh/nearcolor
+link=dynamic
+load=no
+
+objects="nearcolor.o"
diff --git a/Src/init.c b/Src/init.c
index cec914329..e7e62e2f7 100644
--- a/Src/init.c
+++ b/Src/init.c
@@ -94,6 +94,7 @@ mod_export struct hookdef zshhooks[] = {
     HOOKDEF("exit", NULL, HOOKF_ALL),
     HOOKDEF("before_trap", NULL, HOOKF_ALL),
     HOOKDEF("after_trap", NULL, HOOKF_ALL),
+    HOOKDEF("get_color_attr", NULL, HOOKF_ALL),
 };
 
 /* keep executing lists until EOF found */
diff --git a/Src/prompt.c b/Src/prompt.c
index 959ed8e3d..39edbdb2b 100644
--- a/Src/prompt.c
+++ b/Src/prompt.c
@@ -1621,7 +1621,24 @@ match_colour(const char **teststrp, int is_fg, int colour)
     int shft, on, named = 0, tc;
 
     if (teststrp) {
-	if ((named = ialpha(**teststrp))) {
+	if (**teststrp == '#' && isxdigit((*teststrp)[1])) {
+	    struct color_rgb color;
+	    char *end;
+	    zlong col = zstrtol(*teststrp+1, &end, 16);
+            if (end - *teststrp == 4) {
+		color.red = col >> 8 | ((col >> 8) << 4);
+		color.green = (col & 0xf0) >> 4;
+		color.green |= color.green << 4;
+		color.blue = col & 0xf;
+		color.blue |= color.blue << 4;
+	    } else if (end - *teststrp == 7) {
+		color.red = col >> 16;
+		color.green = (col & 0xff00) >> 8;
+		color.blue = col & 0xff;
+	    }
+	    *teststrp = end;
+	    colour = runhookdef(GETCOLORATTR, &color);
+	} else if ((named = ialpha(**teststrp))) {
 	    colour = match_named_colour(teststrp);
 	    if (colour == 8) {
 		/* default */
diff --git a/Src/zsh.h b/Src/zsh.h
index 894158818..68731e226 100644
--- a/Src/zsh.h
+++ b/Src/zsh.h
@@ -2707,6 +2707,12 @@ struct ttyinfo {
 #define COL_SEQ_BG	(1)
 #define COL_SEQ_COUNT	(2)
 
+struct color_rgb {
+    unsigned int red, green, blue;
+};
+
+typedef struct color_rgb *Color_rgb;
+
 /*
  * Flags to testcap() and set_colour_attribute (which currently only
  * handles TSC_PROMPT).
@@ -3203,6 +3209,7 @@ enum {
 #define EXITHOOK       (zshhooks + 0)
 #define BEFORETRAPHOOK (zshhooks + 1)
 #define AFTERTRAPHOOK  (zshhooks + 2)
+#define GETCOLORATTR   (zshhooks + 3)
 
 #ifdef MULTIBYTE_SUPPORT
 /* Final argument to mb_niceformat() */



Messages sorted by: Reverse Date, Date, Thread, Author