diff options
| -rw-r--r-- | ChangeLog | 4 | ||||
| -rw-r--r-- | Doc/Zsh/params.yo | 18 | ||||
| -rw-r--r-- | Doc/Zsh/zle.yo | 61 | ||||
| -rw-r--r-- | Src/Zle/termquery.c | 224 | ||||
| -rw-r--r-- | Src/Zle/zle.h | 26 | ||||
| -rw-r--r-- | Src/Zle/zle_refresh.c | 5 | ||||
| -rw-r--r-- | Src/Zle/zle_vi.c | 1 | ||||
| -rw-r--r-- | Src/init.c | 2 | ||||
| -rw-r--r-- | Src/zsh.h | 4 |
9 files changed, 320 insertions, 25 deletions
@@ -1,5 +1,9 @@ 2025-11-10 Oliver Kiddle <opk@zsh.org> + * 53438: Doc/Zsh/params.yo, Doc/Zsh/zle.yo, Src/Zle/termquery.c, + Src/Zle/zle.h, Src/Zle/zle_refresh.c, Src/Zle/zle_vi.c, Src/init.c, + Src/zsh.h: support for changing terminal cursor shape and colour + * 53404: Doc/Zsh/params.yo, Src/Zle/termquery.c, Src/Zle/zle_main.c, Src/builtin.c, Src/init.c, Src/input.c, Src/loop.c, Src/prompt.c, Src/subst.c, Src/utils.c, Src/zsh.h, Test/X04zlehighlight.ztst, diff --git a/Doc/Zsh/params.yo b/Doc/Zsh/params.yo index 6cb9167d5..76185efca 100644 --- a/Doc/Zsh/params.yo +++ b/Doc/Zsh/params.yo @@ -1749,6 +1749,12 @@ inserted instead of invoking editor commands. Furthermore, pasted text forms a single undo event and if the region is active, pasted text will replace the region. ) +item(tt(cursor-color) <E>)( +Support for changing the color of the cursor. +) +item(tt(cursor-shape) <E>)( +Support for changing the shape of the cursor. +) item(tt(integration-output) <E>)( This provides the terminal with semantic information regarding where the output from commands start and finish. Some terminals use this information to make it @@ -1769,6 +1775,11 @@ item(tt(query-bg) <E>)( Query the terminal background color which is used for tt(.term.bg) and tt(.term.mode). ) +item(tt(query-cursor) <E>)( +Query the cursor color. This facilitates restoring the cursor to its original +color if it has been configured via tt(zle_cursorform). The color is also +assigned to tt(.term.cursor). +) item(tt(query-fg) <E>)( Query the terminal foreground color which is used for tt(.term.fg). ) @@ -1900,6 +1911,13 @@ parameter has the effect of ensuring that bracketed paste remains disabled. However, see also the tt(.term.extensions) parameter which provides a single place to enable or disable terminal features. ) +vindex(zle_cursorform) +item(tt(zle_cursorform))( +An array describing contexts in which ZLE should change the shape and color +of the cursor. +See ifzman(em(Cursor Form) in zmanref(zshzle))\ +ifnzman(noderef(Cursor Form)). +) vindex(zle_highlight) item(tt(zle_highlight))( An array describing contexts in which ZLE should highlight the input text. diff --git a/Doc/Zsh/zle.yo b/Doc/Zsh/zle.yo index 4c5965c6f..669e6927b 100644 --- a/Doc/Zsh/zle.yo +++ b/Doc/Zsh/zle.yo @@ -2855,3 +2855,64 @@ special array parameter tt(region_highlight); see ifnzman(noderef(Zle Widgets))\ ifzman(above). +texinode(Cursor Form)()()(Character Highlighting) +subsect(Cursor Form) +cindex(cursor form) + +vindex(zle_cursorform, setting) +Some terminals support the ability to change the shape and color of the cursor. +On such terminals, the line editor will use cursor styles appropriate to +different contexts. This is controlled via the array parameter +tt(zle_cursorform). To disable all cursor changes, see the tt(.term.extensions) +parameter. + +Each element of the array should consist of a word indicating a context +followed by a colon, then a comma-separated list of properties describing the +shape and color to apply to the cursor. + +The available contexts follow with the default cursor form shown in +parentheses. Where no default is given, the terminal's default is applied: + +startitem() +item(tt(command))( +Used for vi normal mode. +) +item(tt(edit))( +The default form used in the line editor and for editing text in emacs +mode. +) +item(tt(insert) (tt(bar)))( +Used for vi editing mode. +) +item(tt(overwrite) (tt(underline)))( +Used when editing text in overwrite mode or with the vi replace command. +) +item(tt(pending) (tt(underline)))( +Used where the line editor is waiting for a single key press such as the vi +operator pending mode widget. +) +item(tt(region))( +Applied for both tt(regionstart) and tt(regionend) contexts. +) +item(tt(regionstart))( +Used when the region is active and the cursor is positioned at the start of the +region. The region includes the text under the cursor when it is positioned at +the start so it is best to choose a cursor form that does not obscure this fact. +) +item(tt(regionend))( +Used when the region is active and the cursor is positioned at the end of the +region. Note that when this is the case, the region does not include the cursor +position. +) +item(tt(visual))( +Used when vi visual mode is active. The visual selection always includes the +cursor position so the same advice as for tt(regionstart) applies. +) +enditem() + +The available cursor forms are tt(none), tt(bar), tt(block), tt(underline) and +tt(hidden). Additionally, you can specify either tt(blink) or tt(steady) to +indicate whether the cursor should flash and specify a color as an RGB triplet +in hexadecimal format with with tt(color=)var(#xxxxxx). The value tt(none) +applies the terminal's default cursor form. Note that on many terminals, this +may be different to the initial cursor state from when the shell started. diff --git a/Src/Zle/termquery.c b/Src/Zle/termquery.c index c9fd22588..335ea51ed 100644 --- a/Src/Zle/termquery.c +++ b/Src/Zle/termquery.c @@ -132,8 +132,7 @@ typedef const unsigned char seqstate_t; static char *EXTVAR = ".term.extensions"; static char *IDVAR = ".term.id"; static char *VERVAR = ".term.version"; -static char *BGVAR = ".term.bg"; -static char *FGVAR = ".term.fg"; +static char *COLORVAR[] = { ".term.fg", ".term.bg", ".term.cursor" }; static char *MODEVAR = ".term.mode"; /* Query sequences @@ -144,6 +143,7 @@ static char *MODEVAR = ".term.mode"; * because tmux will need to pass these on. */ #define TQ_BGCOLOR "\033]11;?\033\\" #define TQ_FGCOLOR "\033]10;?\033\\" +#define TQ_CURSOR "\033]12;?\033\\" /* Kitty / fixterms keyboard protocol which allows wider support for keys * and modifiers. This clears the screen in terminology. */ @@ -420,32 +420,33 @@ probe_terminal(const char *tquery, seqstate_t *states, settyinfo(&torig); } +static unsigned memo_cursor; + static void handle_color(int bg, int red, int green, int blue) { char *colour; - switch (bg) { - case 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")); - /* fall-through */ - case 0: - colour = zalloc(8); - sprintf(colour, "#%02x%02x%02x", red, green, blue); - setsparam(bg ? BGVAR : FGVAR, colour); - break; - default: break; + 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")); } + + 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); } /* roughly corresponding feature names */ static const char *features[] = - { "bg", "fg", "modkeys-kitty", "truecolor", "id" }; + { "bg", "fg", "cursor", "modkeys-kitty", "truecolor", "id" }; static const char *queries[] = - { TQ_BGCOLOR, TQ_FGCOLOR, TQ_KITTYKB, TQ_RGB, TQ_XTVERSION, TQ_DA }; + { TQ_BGCOLOR, TQ_FGCOLOR, TQ_CURSOR, TQ_KITTYKB, TQ_RGB, TQ_XTVERSION, TQ_DA }; static void handle_query(int sequence, int *numbers, int len, char *capture, int clen, @@ -460,12 +461,12 @@ handle_query(int sequence, int *numbers, int len, char *capture, int clen, break; case 2: /* kitty keyboard */ feat = zshcalloc(2 * sizeof(char *)); - *feat = ztrdup(features[2]); + *feat = ztrdup(features[3]); assignaparam(EXTVAR, feat, ASSPM_WARN|ASSPM_AUGMENT); break; case 3: /* truecolor */ feat = zshcalloc(2 * sizeof(char *)); - *feat = ztrdup(features[3]); + *feat = ztrdup(features[4]); assignaparam(EXTVAR, feat, ASSPM_WARN|ASSPM_AUGMENT); break; case 4: /* id */ @@ -480,7 +481,7 @@ handle_query(int sequence, int *numbers, int len, char *capture, int clen, /**/ void query_terminal(void) { - char tquery[sizeof(TQ_BGCOLOR TQ_FGCOLOR TQ_KITTYKB TQ_RGB TQ_XTVERSION TQ_DA)]; + char tquery[sizeof(TQ_BGCOLOR TQ_FGCOLOR TQ_CURSOR TQ_KITTYKB TQ_RGB TQ_XTVERSION TQ_DA)]; char *tqend = tquery; static seqstate_t states[] = QUERY_STATES; char **f, **flist = getaparam(EXTVAR); @@ -504,7 +505,7 @@ query_terminal(void) { /* if termcap indicates 24-bit color, assume support - even * though this is only based on the initial $TERM * failing that, check $COLORTERM */ - if (i == 3 && (tccolours == 1 << 24 || + if (i == 4 && (tccolours == 1 << 24 || ((cterm = getsparam("COLORTERM")) && (!strcmp(cterm, "truecolor") || !strcmp(cterm, "24bit"))))) @@ -624,7 +625,7 @@ extension_enabled(const char *class, const char *ext, unsigned clen, int def) if (strncmp(*e + negate, class, clen)) continue; - if (!*(*e + negate + clen) || !strcmp(*e + negate + clen, ext)) + if (!*(*e + negate + clen) || !strcmp(*e + negate + clen + 1, ext)) return !negate; } return def; @@ -703,7 +704,7 @@ end_edit(void) const char ** prompt_markers(void) { - static unsigned aid = 0; + static unsigned int aid = 0; static char pre[] = "\033]133;A;cl=m;aid=zZZZZZZ\033\\"; /* before the prompt */ static const char *const PR = "\033]133;P;k=i\033\\"; /* primary (PS1) */ static const char *const SE = "\033]133;P;k=s\033\\"; /* secondary (PS2) */ @@ -754,3 +755,180 @@ notify_pwd(void) write_loop(SHTTY, url, ulen); write_loop(SHTTY, "\033\\", 2); } + +static unsigned int *cursor_forms; +static unsigned int cursor_enabled_mask; + +static void +match_cursorform(const char *teststr, unsigned int *cursor_form) +{ + static const struct { + const char *name; + unsigned char value, mask; + } shapes[] = { + { "none", 0, 0xff }, + { "underline", CURF_UNDERLINE, CURF_SHAPE_MASK }, + { "bar", CURF_BAR, CURF_SHAPE_MASK }, + { "block", CURF_BLOCK, CURF_SHAPE_MASK }, + { "blink", CURF_BLINK, CURF_STEADY }, + { "steady", CURF_STEADY, CURF_BLINK }, + { "hidden", CURF_HIDDEN, 0 } + }; + + *cursor_form = 0; + while (*teststr) { + size_t s; + int found = 0; + + if (strpfx("color=#", teststr)) { + char *end; + teststr += 7; + zlong col = zstrtol(teststr, &end, 16); + if (end - teststr == 4) { + unsigned int red = col >> 8; + unsigned int green = (col & 0xf0) >> 4; + unsigned int blue = (col & 0xf); + *cursor_form &= 0xff; /* clear color */ + *cursor_form |= CURF_COLOR | + ((red << 4 | red) << CURF_RED_SHIFT) | + ((green << 4 | green) << CURF_GREEN_SHIFT) | + ((blue << 4 | blue) << CURF_BLUE_SHIFT); + found = 1; + } else if (end - teststr == 6) { + *cursor_form |= (col << 8) | CURF_COLOR; + found = 1; + } + teststr = end; + } + for (s = 0; !found && s < sizeof(shapes) / sizeof(*shapes); s++) { + if (strpfx(shapes[s].name, teststr)) { + teststr += strlen(shapes[s].name); + *cursor_form &= ~shapes[s].mask; + *cursor_form |= shapes[s].value; + found = 1; + } + } + if (!found) /* skip an unknown component */ + teststr = strchr(teststr, ','); + if (!teststr || *teststr != ',') + break; + teststr++; + } +} + +/**/ +void +zle_set_cursorform(void) +{ + char **atrs = getaparam("zle_cursorform"); + static int setup = 0; + size_t i; + static const char *contexts[] = { + "edit:", + "command:", + "insert:", + "overwrite:", + "pending:", + "regionstart:", + "regionend:", + "visual:" + }; + + if (!cursor_forms) + cursor_forms = zalloc(CURC_DEFAULT * sizeof(*cursor_forms)); + memset(cursor_forms, 0, CURC_DEFAULT * sizeof(*cursor_forms)); + cursor_forms[CURC_INSERT] = CURF_BAR; + cursor_forms[CURC_OVERWRITE] = CURF_UNDERLINE; + cursor_forms[CURC_PENDING] = CURF_UNDERLINE; + + for (; atrs && *atrs; atrs++) { + if (strpfx("region:", *atrs)) { + match_cursorform(*atrs + 7, &cursor_forms[CURC_REGION_END]); + cursor_forms[CURC_REGION_START] = cursor_forms[CURC_REGION_END]; + continue; + } + for (i = 0; i < sizeof(contexts) / sizeof(*contexts); i++) { + if (strpfx(contexts[i], *atrs)) { + match_cursorform(*atrs + strlen(contexts[i]), &cursor_forms[i]); + break; + } + } + } + + if (!setup || trashedzle) { + cursor_enabled_mask = 0; + setup = 1; + if (!extension_enabled("cursor", "shape", 6, 1)) + cursor_enabled_mask |= CURF_SHAPE_MASK | CURF_BLINK | CURF_STEADY; + if (!extension_enabled("cursor", "color", 6, 1)) + cursor_enabled_mask |= CURF_COLOR_MASK; + } +} + +/**/ +void +free_cursor_forms(void) +{ + if (cursor_forms) + zfree(cursor_forms, CURC_DEFAULT * sizeof(*cursor_form)); + cursor_forms = 0; +} + +/**/ +void +cursor_form(void) +{ + char seq[31]; + char *s = seq; + unsigned int want, changed; + static unsigned int state = CURF_DEFAULT; + enum cursorcontext context = CURC_DEFAULT; + + if (!cursor_forms) + return; + + if (trashedzle) { + ; + } else if (!insmode) { + context = CURC_OVERWRITE; + } else if (vichgflag == 2) { + context = CURC_PENDING; + } else if (region_active) { + if (invicmdmode()) { + context = CURC_VISUAL; + } else { + context = mark > zlecs ? CURC_REGION_START : CURC_REGION_END; + } + } else + context = invicmdmode() ? CURC_COMMAND : (vichgflag ? CURC_INSERT : CURC_EDIT); + want = (context == CURC_DEFAULT) ? CURF_DEFAULT : cursor_forms[context]; + if (!(changed = (want ^ state) & ~cursor_enabled_mask)) + return; + + if (changed & CURF_HIDDEN) + tcout(want & CURF_HIDDEN ? TCCURINV : TCCURVIS); + if (changed & CURF_SHAPE_MASK) { + char c = '0'; + switch (want & CURF_SHAPE_MASK) { + case CURF_BAR: c += 2; + case CURF_UNDERLINE: c += 2; + case CURF_BLOCK: + c += 2 - !!(want & CURF_BLINK); + changed &= ~(CURF_BLINK | CURF_STEADY); + } + s += sprintf(s, "\033[%c q", c); + } + if (changed & (CURF_BLINK | CURF_STEADY)) { + s += sprintf(s, "\033[?12%c", (want & CURF_BLINK) ? 'h' : 'l'); + } + if (changed & CURF_COLOR_MASK) { + if (!(want & CURF_COLOR_MASK)) + want = memo_cursor | (want & 0xff); + s += sprintf(s, "\033]12;rgb:%02x00/%02x00/%02x00\033\\", + want >> CURF_RED_SHIFT, (want >> CURF_GREEN_SHIFT) & 0xff, + (want >> CURF_BLUE_SHIFT) & 0xff); + } + if (s - seq) + write_loop(SHTTY, seq, s - seq); + state = want; +} diff --git a/Src/Zle/zle.h b/Src/Zle/zle.h index 5bb9e7a5e..91eefc9e5 100644 --- a/Src/Zle/zle.h +++ b/Src/Zle/zle.h @@ -470,6 +470,32 @@ struct region_highlight { * interaction in Doc/Zsh/zle.yo. */ #define N_SPECIAL_HIGHLIGHTS (4) +/* Terminal cursor contexts */ +enum cursorcontext { + CURC_EDIT, + CURC_COMMAND, + CURC_INSERT, + CURC_OVERWRITE, + CURC_PENDING, + CURC_REGION_START, + CURC_REGION_END, + CURC_VISUAL, + CURC_DEFAULT +}; + +#define CURF_DEFAULT 0 +#define CURF_UNDERLINE 1 +#define CURF_BAR 2 +#define CURF_BLOCK 3 +#define CURF_SHAPE_MASK 3 +#define CURF_BLINK (1 << 2) +#define CURF_STEADY (1 << 3) +#define CURF_HIDDEN (1 << 4) +#define CURF_COLOR (1 << 5) +#define CURF_COLOR_MASK ((0xffffffu << 8) | CURF_COLOR) +#define CURF_RED_SHIFT 24 +#define CURF_GREEN_SHIFT 16 +#define CURF_BLUE_SHIFT 8 #ifdef MULTIBYTE_SUPPORT /* diff --git a/Src/Zle/zle_refresh.c b/Src/Zle/zle_refresh.c index f076bdd61..f27191114 100644 --- a/Src/Zle/zle_refresh.c +++ b/Src/Zle/zle_refresh.c @@ -1024,6 +1024,8 @@ zrefresh(void) tmpalloced = 0; } + zle_set_cursorform(); + /* this will create region_highlights if it's still NULL */ zle_set_highlight(); @@ -1666,6 +1668,7 @@ individually */ /* move to the new cursor position */ moveto(rpms.nvln, rpms.nvcs); + cursor_form(); /* swap old and new buffers - better than freeing/allocating every time */ bufswap(); @@ -2706,4 +2709,6 @@ zle_refresh_finish(void) region_highlights = NULL; n_region_highlights = 0; } + + free_cursor_forms(); } diff --git a/Src/Zle/zle_vi.c b/Src/Zle/zle_vi.c index 6692df830..667063774 100644 --- a/Src/Zle/zle_vi.c +++ b/Src/Zle/zle_vi.c @@ -186,6 +186,7 @@ getvirange(int wf) virangeflag = 1; wordflag = wf; mark = -1; + cursor_form(); /* use operator-pending keymap if one exists */ Keymap km = openkeymap("viopp"); if (km) diff --git a/Src/init.c b/Src/init.c index dea52202e..20b8cc7fd 100644 --- a/Src/init.c +++ b/Src/init.c @@ -754,7 +754,7 @@ static char *tccapnams[TC_COUNT] = { "cl", "le", "LE", "nd", "RI", "up", "UP", "do", "DO", "dc", "DC", "ic", "IC", "cd", "ce", "al", "dl", "ta", "md", "mh", "so", "us", "ZH", "me", "se", "ue", "ZR", "ch", - "ku", "kd", "kl", "kr", "sc", "rc", "bc", "AF", "AB" + "ku", "kd", "kl", "kr", "sc", "rc", "bc", "AF", "AB", "vi", "ve" }; /**/ @@ -2674,7 +2674,9 @@ struct ttyinfo { #define TCBACKSPACE 34 #define TCFGCOLOUR 35 #define TCBGCOLOUR 36 -#define TC_COUNT 37 +#define TCCURINV 37 +#define TCCURVIS 38 +#define TC_COUNT 39 #define tccan(X) (tclen[X]) |
