Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
PATCH: colour mixing with layers
- X-seq: zsh-workers 54074
- From: Oliver Kiddle <opk@xxxxxxx>
- To: Zsh workers <zsh-workers@xxxxxxx>
- Subject: PATCH: colour mixing with layers
- Date: Thu, 13 Nov 2025 03:11:37 +0100
- Archived-at: <https://zsh.org/workers/54074>
- List-id: <zsh-workers.zsh.org>
The following adds an opaque= key for use in zle_highlight/region_highlight
which will use a blend of the new and inherited colours.
So for example a widget that uses recursive-edit to allow history
retrievals in the middle of blocks can grey-out the pre/post buffer.
Or you might brighten or darken text colours to keep them readable as
part of the highlighting for the region.
Opacity alone is fairly flexible - with grey shades as your mixer, you
can lighten/darken or increase/reduce contrast. But I'd be interested to
hear if anyone has other, related ideas for colour manipulation.
The syntax is e.g. opacity=40% (applys to foreground and background),
opacity=10%/90% (for alternate values). For background only, either
don't use fg= or give 100% for the foreground value. The '%' is
optional. I considered other formats like #RRGGBBAA or fgopacity= and
bgopacity= but I think this is easier for simple cases. A comma is not
an option for the separator because that's already used. Any opinions on
the `/'?
Mixing is done in the RGB colour space which, from what I understand,
is not too bad for this particular use-case. The colour space used by
nearcolor would better match human perception. For gradients there are
better options for linear light intensity but you're not going to get
smooth gradients anyway when colours are applied to whole character
spaces.
This is only implemented for truecolor unless you count the 50%
comparison fallback. It doesn't currently work together with nearcolor.
I may yet try that. Is it better to do the conversion based on the
colour as it appears after conversion or based on the original specified
hex-triplet? The former is less problematic given the point where
nearcolor is called.
If the terminal queries were able to retrieve the default colour on
startup, that can be mixed. That won't work in prompts via %H. I'm not
sure whether that's useful enough to add.
Oliver
diff --git a/Doc/Zsh/zle.yo b/Doc/Zsh/zle.yo
index 315048887..a0d97a0c8 100644
--- a/Doc/Zsh/zle.yo
+++ b/Doc/Zsh/zle.yo
@@ -2905,7 +2905,14 @@ With the other fields 30 applies by default for tt(special), 20 for tt(region)
and tt(isearch) and 15 for tt(paste). Highlighting defined in
tt(region_highlight) defaults to layer 10 and would take precedence over
highlighting for any fields of tt(zle_highlight) that are assigned to the same
-layer.)
+layer.
+)
+item(tt(opacity=)var(fg)tt(%)[tt(/)var(bg)tt(%)])(
+Instead of replacing colors in higher layers, the colors can be mixed. The
+opacity is specified as a percentage where tt(0%) is fully transparent and
+tt(100%) represents the default behavior of replacing the underlying colour.
+If a single value is specified, it applies to both foreground and background.
+)
enditem()
In addition, the simple highlighting types can be prefixed with tt("no") to
diff --git a/Src/Zle/termquery.c b/Src/Zle/termquery.c
index c6f83044d..ea8945895 100644
--- a/Src/Zle/termquery.c
+++ b/Src/Zle/termquery.c
@@ -439,16 +439,26 @@ handle_color(int bg, int red, int green, int blue)
{
char *colour;
- if (bg == 1) { /* background color */
- /* scale by Rec.709 coefficients for lightness */
- setsparam(MODEVAR, ztrdup(
- 0.2126f * red + 0.7152f * green + 0.0722f * blue <= 127 ?
- "dark" : "light"));
+ switch (bg) {
+ case 0: /* foreground color */
+ memo_term_color &= ~TXT_ATTR_FG_MASK;
+ memo_term_color |= TXT_ATTR_FG_24BIT | (zattr) ((((red << 8)
+ + green) << 8) + blue) << TXT_ATTR_FG_COL_SHIFT;
+ break;
+ case 1: /* background color */
+ memo_term_color &= ~TXT_ATTR_BG_MASK;
+ memo_term_color |= TXT_ATTR_BG_24BIT | (zattr) ((((red << 8)
+ + green) << 8) + blue) << TXT_ATTR_BG_COL_SHIFT;
+ /* scale by Rec.709 coefficients for lightness */
+ setsparam(MODEVAR, ztrdup(
+ 0.2126f * red + 0.7152f * green + 0.0722f * blue <= 127 ?
+ "dark" : "light"));
+ break;
+ case 2: /* cursor color */
+ memo_cursor = (red << 24) | (green << 16) | (blue << 8);
+ break;
}
- if (bg == 2) /* cursor color */
- memo_cursor = (red << 24) | (green << 16) | (blue << 8);
-
colour = zalloc(8);
sprintf(colour, "#%02x%02x%02x", red, green, blue);
setsparam(COLORVAR[bg], colour);
diff --git a/Src/Zle/zle_main.c b/Src/Zle/zle_main.c
index 46d0e07d2..9ad917d86 100644
--- a/Src/Zle/zle_main.c
+++ b/Src/Zle/zle_main.c
@@ -1277,7 +1277,7 @@ zleread(char **lp, char **rp, int flags, int context, char *init, char *finish)
raw_rp = rp;
rpromptbuf = promptexpand(rp ? *rp : NULL, 1, markers[2], NULL, NULL);
rpmpt_attr = txtcurrentattrs;
- prompt_attr = mixattrs(pmpt_attr, pmpt_attr, rpmpt_attr);
+ prompt_attr = mixattrs(pmpt_attr, pmpt_attr & TXT_ATTR_ALL, rpmpt_attr);
free_prepostdisplay();
zlereadflags = flags;
@@ -2032,7 +2032,7 @@ reexpandprompt(void)
new_rprompt = promptexpand(raw_rp ? *raw_rp : NULL, 1, markers[2], NULL, NULL);
rpmpt_attr = txtcurrentattrs;
- prompt_attr = mixattrs(pmpt_attr, pmpt_attr, rpmpt_attr);
+ prompt_attr = mixattrs(pmpt_attr, pmpt_attr & TXT_ATTR_ALL, rpmpt_attr);
free(rpromptbuf);
rpromptbuf = new_rprompt;
} while (looping != reexpanding);
diff --git a/Src/Zle/zle_refresh.c b/Src/Zle/zle_refresh.c
index 8a89be333..255c701f2 100644
--- a/Src/Zle/zle_refresh.c
+++ b/Src/Zle/zle_refresh.c
@@ -208,7 +208,7 @@ int predisplaylen, postdisplaylen;
* and for ellipsis continuation markers.
*/
-static zattr default_attr, special_attr, special_mask, ellipsis_attr;
+static zattr default_attr, default_mask, special_attr, special_mask, ellipsis_attr;
/*
* Layer applied to highlighting for special characters
@@ -330,7 +330,7 @@ zle_set_highlight(void)
int ellipsis_attr_set = 0;
struct region_highlight *rhp;
- special_attr = default_attr = 0;
+ special_attr = default_attr = special_mask = default_mask = 0;
if (!region_highlights) {
region_highlights = (struct region_highlight *)
zshcalloc(N_SPECIAL_HIGHLIGHTS*sizeof(struct region_highlight));
@@ -354,12 +354,12 @@ zle_set_highlight(void)
for (; *atrs; atrs++) {
if (!strcmp(*atrs, "none")) {
/* reset attributes for consistency... usually unnecessary */
- special_attr = default_attr = 0;
+ special_attr = default_attr = special_mask = default_mask = 0;
special_attr_set = 1;
paste_attr_set = region_attr_set =
isearch_attr_set = suffix_attr_set = 1;
} else if (strpfx("default:", *atrs)) {
- match_highlight(*atrs + 8, &default_attr, NULL, NULL);
+ match_highlight(*atrs + 8, &default_attr, &default_mask, NULL);
} else if (strpfx("special:", *atrs)) {
match_highlight(*atrs + 8, &special_attr, &special_mask,
&special_layer);
@@ -1206,7 +1206,7 @@ zrefresh(void)
rpms.s = nbuf[rpms.ln = 0] + lpromptw;
rpms.sen = *nbuf + winw;
for (t = tmpline, tmppos = 0; tmppos < tmpll; t++, tmppos++) {
- zattr base_attr = mixattrs(default_attr, default_attr, prompt_attr);
+ zattr base_attr = mixattrs(default_attr, default_mask, prompt_attr);
zattr all_attr = 0;
struct region_highlight *rhp;
int layer, nextlayer = 0;
@@ -2452,7 +2452,7 @@ singlerefresh(ZLE_STRING_T tmpline, int tmpll, int tmpcs)
for (t0 = 0; t0 < tmpll; t0++) {
unsigned ireg;
- zattr base_attr = 0;
+ zattr base_attr = mixattrs(default_attr, default_attr, prompt_attr);
zattr all_attr;
struct region_highlight *rhp;
/*
diff --git a/Src/prompt.c b/Src/prompt.c
index 83a7667cc..161f49971 100644
--- a/Src/prompt.c
+++ b/Src/prompt.c
@@ -45,6 +45,11 @@ mod_export zattr txtpendingattrs;
/**/
mod_export zattr txtunknownattrs;
+/* detected default attributes for the terminal if any */
+
+/**/
+mod_export zattr memo_term_color;
+
/* the command stack for use with %_ in prompts */
/**/
@@ -1768,16 +1773,57 @@ tunsetattrs(zattr newattrs)
mod_export zattr
mixattrs(zattr primary, zattr mask, zattr secondary)
{
- zattr select = mask & TXT_ATTR_ALL;
+ zattr mix = 0; /* attributes resulting from colour mixing */
+ zattr keep; /* attributes from secondary */
+ zattr replace = mask & TXT_ATTR_ALL; /* attributes from primary */
+ zattr toset = TXT_ATTR_FG_MASK;
+ zattr isset = TXTFGCOLOUR;
+ zattr istrue = TXT_ATTR_FG_24BIT;
+ unsigned int shift = TXT_ATTR_FG_COL_SHIFT;
+ int opacity, i;
- if (mask & TXTFGCOLOUR)
- select |= TXT_ATTR_FG_MASK;
- if (mask & TXTBGCOLOUR)
- select |= TXT_ATTR_BG_MASK;
if (mask & TXT_ATTR_FONT_WEIGHT)
- select |= TXT_ATTR_FONT_WEIGHT;
+ replace |= TXT_ATTR_FONT_WEIGHT;
+ if (mask & TXTFGCOLOUR)
+ replace |= TXT_ATTR_FG_MASK;
+ if (mask & TXTBGCOLOUR)
+ replace |= TXT_ATTR_BG_MASK;
+ keep = ~replace;
- return (primary & select) | (secondary & ~select);
+ do {
+ if ((opacity = (mask >> shift) & 127)) {
+ zattr argb, brgb;
+ /* we may know the default colours from the startup query */
+ zattr aatt = (primary & isset) ? primary : memo_term_color;
+ zattr batt = (secondary & isset) ? secondary : memo_term_color;
+
+ keep &= ~toset;
+ replace &= ~toset;
+ /* can only mix truecolor */
+ if (aatt & batt & istrue) {
+ mix |= istrue | isset;
+ for (i = 0; i < 3; i++, shift += 8) {
+ argb = (aatt >> shift) & 0xff;
+ brgb = (batt >> shift) & 0xff;
+ mix |= ((argb * (100 - opacity) + brgb * opacity) / 100)
+ << shift;
+ }
+ } else if (opacity <= 50)
+ replace |= toset;
+ else
+ keep |= toset;
+ }
+
+ if (isset == TXTBGCOLOUR)
+ break;
+
+ shift = TXT_ATTR_BG_COL_SHIFT;
+ toset = TXT_ATTR_BG_COL_MASK;
+ isset = TXTBGCOLOUR;
+ istrue = TXT_ATTR_BG_24BIT;
+ } while (1);
+
+ return (primary & replace) | (secondary & keep) | mix;
}
/*****************************************************************************
@@ -1962,9 +2008,11 @@ match_highlight(const char *teststr, zattr *on_var, zattr *setmask, int *layer)
break;
found = 1;
/* skip out of range colours but keep scanning attributes */
- if (atr != TXT_ERROR)
+ if (atr != TXT_ERROR) {
+ *on_var &= is_fg ? ~TXT_ATTR_FG_MASK : ~TXT_ATTR_BG_MASK;
*on_var |= atr;
- mask |= is_fg ? TXTFGCOLOUR : TXTBGCOLOUR;
+ mask |= is_fg ? TXTFGCOLOUR : TXTBGCOLOUR;
+ }
} else if (layer && strpfx("layer=", teststr)) {
teststr += 6;
*layer = (int) zstrtol(teststr, (char **) &teststr, 10);
@@ -1973,6 +2021,29 @@ match_highlight(const char *teststr, zattr *on_var, zattr *setmask, int *layer)
else if (*teststr && *teststr != ' ')
break;
found = 1;
+ } else if (strpfx("opacity=", teststr)) {
+ teststr += 8;
+ zulong opacity = zstrtol(teststr, (char **) &teststr, 10);
+ if (opacity > 100)
+ break;
+ if (*teststr == '%')
+ teststr++;
+ /* invert sense so 0 is fully opaque */
+ mask |= (100 - opacity) << TXT_ATTR_FG_COL_SHIFT;
+ if (*teststr == '/') {
+ teststr++;
+ opacity = zstrtol(teststr, (char **) &teststr, 10);
+ if (opacity > 100)
+ break;
+ if (*teststr == '%')
+ teststr++;
+ }
+ mask |= (100 - opacity) << TXT_ATTR_BG_COL_SHIFT;
+ if (*teststr == ',')
+ teststr++;
+ else if (*teststr && *teststr != ' ')
+ break;
+ found = 1;
} else {
int turn_off = 0;
for (hl = highlights; !found && hl->name; hl++) {
@@ -2154,6 +2225,30 @@ output_highlight(zattr atr, zattr mask, char *buf)
strcpy(ptr, "none");
return 4;
}
+
+ if (mask & (TXT_ATTR_FG_COL_MASK | TXT_ATTR_BG_COL_MASK)) {
+ unsigned fg_op, bg_op;
+ char tmp[13];
+ size_t len;
+
+ if (atrlen) {
+ atrlen++;
+ if (buf) {
+ strcpy(ptr, ",");
+ ptr++;
+ }
+ }
+ atrlen += 8;
+ fg_op = (mask >> TXT_ATTR_FG_COL_SHIFT) & 127;
+ bg_op = (mask >> TXT_ATTR_BG_COL_SHIFT) & 127;
+ len = sprintf(buf ? ptr : tmp, "opacity=%u%%", 100 - fg_op);
+ atrlen += len;
+ if (buf)
+ ptr += len;
+ if (fg_op != bg_op)
+ atrlen += sprintf(buf ? ptr : tmp, "/%u%%", 100 - bg_op);
+ }
+
return atrlen;
}
Messages sorted by:
Reverse Date,
Date,
Thread,
Author