From 5411128bf3c83516da8fdc4cb3d6187f243b6954 Mon Sep 17 00:00:00 2001 From: Oliver Kiddle Date: Mon, 17 Nov 2025 04:32:14 +0100 Subject: 54074, 54082: add an opaque key for use in zle_highlight/region_highlight to allow mixing of colours --- ChangeLog | 5 ++ Doc/Zsh/zle.yo | 9 ++- Src/Modules/nearcolor.c | 2 +- Src/Zle/termquery.c | 26 +++++--- Src/Zle/zle_main.c | 4 +- Src/Zle/zle_refresh.c | 12 ++-- Src/prompt.c | 157 +++++++++++++++++++++++++++++++++++++++++++--- Test/X04zlehighlight.ztst | 84 +++++++++++++++++++++++++ 8 files changed, 273 insertions(+), 26 deletions(-) diff --git a/ChangeLog b/ChangeLog index e6b43936e..f0e744d6c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,10 @@ 2025-11-17 Oliver Kiddle + * 54074, 54082: Doc/Zsh/zle.yo, Src/prompt.c, Src/Zle/termquery.c, + Src/Zle/zle_main.c, Src/Zle/zle_refresh.c, Src/Modules/nearcolor.c, + Test/X04zlehighlight.ztst: add an opaque key for use in + zle_highlight/region_highlight to allow mixing of colours + * 54083: Src/Zle/zle_keymap.c: fix for cursor shape in viopp mode * 54075: Doc/Zsh/prompt.yo, Src/Modules/watch.c, Src/Zle/complist.c, 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/Modules/nearcolor.c b/Src/Modules/nearcolor.c index d50a6bb44..83d7118b0 100644 --- a/Src/Modules/nearcolor.c +++ b/Src/Modules/nearcolor.c @@ -146,7 +146,7 @@ mapRGBto256(int red, int green, int blue) static int getnearestcolor(UNUSED(Hookdef dummy), Color_rgb col) { - /* we add 1 to the colours so that colour 0 (black) is + /* we add 1 to the colours so that colour 0 (default) is * distinguished from runhookdef() indicating that no * hook function is registered */ if (tccolours == 256) 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 e4e3feb89..c3c34abf2 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 */ /**/ @@ -1759,6 +1764,32 @@ tunsetattrs(zattr newattrs) txtpendingattrs &= ~TXT_ATTR_BG_MASK; } +void +map256toRGB(zattr *atr, int shift, zattr set24) +{ + unsigned colour, red, green, blue; + + if (*atr & set24) + return; + + if ((colour = ((*atr >> shift) & 0xff)) < 16) + return; + + if (colour >= 16 && colour < 232) { + colour -= 16; + blue = !!colour * 0x37 + 40 * (colour % 6); + colour /= 6; + green = !!colour * 0x37 + 40 * (colour % 6); + colour /= 6; + red = !!colour * 0x37 + 40 * colour; + } else { + red = green = blue = 8 + 10 * (colour - 232); + } + + *atr &= ~((zattr) 0xffffff << shift); + *atr |= set24 | (zattr) ((((red << 8) + green) << 8) + blue) << shift; +} + /* Merge two attribute sets. * secondary is the background base attributes * primary is attributes to be overlaid, taking precedence. @@ -1770,16 +1801,76 @@ 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 & TXT_ATTR_FONT_WEIGHT) + replace |= TXT_ATTR_FONT_WEIGHT; if (mask & TXTFGCOLOUR) - select |= TXT_ATTR_FG_MASK; + replace |= 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_BG_MASK; + keep = ~replace; + + do { + if (mask & isset && (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; + + if (tccolours == 256) { + map256toRGB(&aatt, shift, istrue); + map256toRGB(&batt, shift, istrue); + } + + /* can only mix if we now have truecolor */ + if (aatt & batt & istrue) { + mix |= istrue | isset; + for (i = 0; i < 24; i += 8) { + argb = (aatt >> (shift + i)) & 0xff; + brgb = (batt >> (shift + i)) & 0xff; + mix |= ((argb * (100 - opacity) + brgb * opacity) / 100) + << (shift + i); + } + if (!truecolor_terminal() && (!empty(GETCOLORATTR->funcs) || + !load_module("zsh/nearcolor", NULL, 1))) { + struct color_rgb color = { + (mix >> (shift + 16)) & 0xff, + (mix >> (shift + 8)) & 0xff, + (mix >> shift) & 0xff + }; + int color_idx = runhookdef(GETCOLORATTR, &color) - 1; + if (color_idx >= 0) { + mix &= ~toset; + mix |= isset | ((zattr) color_idx << shift); + } + } + } else if (opacity <= 50) + replace |= toset; + else + keep |= toset; + } + + if (isset == TXTBGCOLOUR) + break; - return (primary & select) | (secondary & ~select); + 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; } /***************************************************************************** @@ -1839,6 +1930,7 @@ match_named_colour(const char **teststrp) return -1; } +/**/ static int truecolor_terminal() { @@ -1964,9 +2056,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 ? (zattr) ~TXT_ATTR_FG_MASK : (zattr) ~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); @@ -1975,6 +2069,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++) { @@ -2156,6 +2273,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; } diff --git a/Test/X04zlehighlight.ztst b/Test/X04zlehighlight.ztst index ecb309605..bfb3dd135 100644 --- a/Test/X04zlehighlight.ztst +++ b/Test/X04zlehighlight.ztst @@ -93,6 +93,46 @@ 0:basic region_highlight with 8 colors >0mCDE|32|true + zpty_start + zpty_input 'zle-line-pre-redraw() { BUFFER=": abcde" region_highlight=( "2 5 faint,bold" ) }' + zpty_input 'zle -N zle-line-pre-redraw' + zpty_enable_zle + zpty_input $'\C-a' + zpty_line 1 p + zpty_stop +0:later highlight attribute takes precedence over earlier one that contradicts +>0m: 1mabc0mde + + zpty_start + zpty_input 'zle-line-pre-redraw() { BUFFER=": abcde" region_highlight=( "2 5 faint,unrecognised,bold" ) }' + zpty_input 'zle -N zle-line-pre-redraw' + zpty_enable_zle + zpty_input $'\C-a' + zpty_line 1 p + zpty_stop +0:unrecognised highlight attribute and any subsequent ones ignored +>0m: 2mabc0mde + + zpty_start + zpty_input 'zle-line-pre-redraw() { BUFFER=": abcd" region_highlight=( "2 5 bold" "3 4 nobold" ) }' + zpty_input 'zle -N zle-line-pre-redraw' + zpty_enable_zle + zpty_input $'\C-a' + zpty_line 1 p + zpty_stop +0:disable highlight attribute with "no" prefix +>0m: 1ma0mb1mc0md + + zpty_start + zpty_input 'zle-line-pre-redraw() { BUFFER=": abcdefg" region_highlight=( "2 5 faint,layer=14" "3 4 bold,layer=12" ) }' + zpty_input 'zle -N zle-line-pre-redraw' + zpty_enable_zle + zpty_input $'\C-a' + zpty_line 1 p + zpty_stop +0:use layers to override precedence of ordering +>0m: 2mabc0mdefg + zpty_start zpty_input 'rh_widget() { region_highlight+=( "0 4 fg=green memo=someplugin" ); typeset -p region_highlight }' zpty_input 'zle -N rh_widget' @@ -160,6 +200,18 @@ 0:basic region_highlight with near-color (hex-triplets at input) >0mCDE|3232|true + zpty_start + zpty_input 'zmodload zsh/nearcolor' + zpty_input 'rh_widget() { BUFFER="true"; region_highlight+=( "0 4 fg=#000000,bg=#ffffff" ); }' + zpty_input 'zle -N rh_widget' + zpty_input 'bindkey "\C-a" rh_widget' + zpty_enable_zle + zpty_input $'\C-a' # emits newline, which executes BUFFER="true" command + zpty_line 1 p # the line of interest, preserving escapes ("p") + zpty_stop +0:basic region_highlight with near-color using extremes - all black and all white +>0mCDE|316|BCDE|4231|true + zpty_start zpty_input 'rh_widget() { BUFFER="true"; region_highlight+=( "0 4 fg=green" ); rh2; }' zpty_input 'rh2() { region_highlight+=( "1 2 fg=red" ); }' # `r' in red; the above line would be too long @@ -208,6 +260,38 @@ 0:zle $widgetname -f nolast >0m0m: clear-screen + zpty_start + zpty_input '.term.extensions[(r)truecolor]=-truecolor' + zpty_input 'zle-line-pre-redraw() { BUFFER=": abc" region_highlight=( "2 4 fg=230" "3 5 fg=132,opacity=50%" ) }' + zpty_input 'zle -N zle-line-pre-redraw' + zpty_enable_zle + zpty_input $'\C-a' + zpty_line 1 p + zpty_stop +0:overlapping backgrounds with opacity and nearcolor +>0m: CDE|3230|aCDE|3181|bCDE|3132|c + + zpty_start + zpty_input '.term.extensions[(r)truecolor]=-truecolor' + zpty_input 'zle-line-pre-redraw() { BUFFER=": abcde" region_highlight=( "3 4 fg=149,opacity=80" "5 6 fg=182,opacity=49" ) }' + zpty_input 'zle -N zle-line-pre-redraw' + zpty_enable_zle + zpty_input $'\C-a' + zpty_line 1 p + zpty_stop +0:opacity with an unknown base colour +>0m: aCDE|3149|b0mcde + + zpty_start + zpty_input 'zle-line-pre-redraw() { BUFFER=": abcde" region_highlight=( "2 7 bg=#d5e7c2,fg=#00005f,bold" "4 6 bg=#e0ff50,fg=#e00020,opacity=60/40" ) }' + zpty_input 'zle -N zle-line-pre-redraw' + zpty_enable_zle + zpty_input $'\C-a' + zpty_line 1 p + zpty_stop +0:foreground and background opacity +>0m: 1m38;2;0;0;95m48;2;213;231;194mab38;2;134;0;57m48;2;217;240;148mcd38;2;0;0;95m48;2;213;231;194me + %clean zmodload -ui zsh/zpty -- cgit v1.2.3-70-g09d2