53438: support for changing terminal cursor shape and colour

This commit is contained in:
Oliver Kiddle
2025-11-10 21:02:34 +01:00
parent 6a691a3487
commit ba008572e8
9 changed files with 320 additions and 25 deletions

View File

@@ -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,

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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
/*

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -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"
};
/**/

View File

@@ -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])