Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
PATCH: changing terminal cursor form
- X-seq: zsh-workers 53438
- From: Oliver Kiddle <opk@xxxxxxx>
- To: Zsh workers <zsh-workers@xxxxxxx>
- Subject: PATCH: changing terminal cursor form
- Date: Thu, 03 Apr 2025 00:45:40 +0200
- Archived-at: <https://zsh.org/workers/53438>
- List-id: <zsh-workers.zsh.org>
Most terminals allow the cursor shape and colour to be changed. This is
especially useful in vi mode as a way to distinguish between insert and
command modes. I've had this configured in my own setup since long ago
but there are limitations when configuring this in shell code and some
aspects are tricky to get right judging by the number of imperfect
examples I've seen out there.
In terms of supported escape sequences, the VT520 DECSCUSR sequence is
now widely supported (\e[0 q). A separate sequence starting \e[?12 for
controlling blink and setting the colour is also widely supported. This
patch also includes hiding the cursor for completeness and that's an old
sequence covered by termcap. The Linux console has it's own sequences
which also allow a half-block and you may also see sequences such as
Konsole's OSC 50 (\e]50;CursorShape=1\a) which due to a clash with other
xterm sequences was changed to OSC 1337. I've not found any terminal
that uses that but doesn't also support the DECSCUSR sequence.
This adds a query for the cursor colour to startup. This allows us
to restore the original cursor colour. It isn't always possible to
reliably restore the cursor state. Sometimes "\e[0 q" does this but
the original VT520 documentation defines both 0 and 2 as being a block
cursor so many terminals will revert to a block. Both steady and blink
are defined separately because you might want to force a blinking or
steady cursor without deviating from the default shape and the initial
state is unknown.
Configuration is via a zle_cursorform variable similar to zle_highlight.
Much of the implementation follows the same path as for zle_highlight.
The value of that variable gets freshly parsed rather more often than
I'd have thought was necessary.
This depends on earlier patches that have yet to be applied pending
a solution to the problem of handling SIGINT while awaiting terminal
responses.
Oliver
diff --git a/Doc/Zsh/params.yo b/Doc/Zsh/params.yo
index 46ad9cfde..43fcd8b0b 100644
--- a/Doc/Zsh/params.yo
+++ b/Doc/Zsh/params.yo
@@ -1754,6 +1754,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
@@ -1774,6 +1780,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).
)
@@ -1914,6 +1925,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 316d232c3..64c6dfafc 100644
--- a/Doc/Zsh/zle.yo
+++ b/Doc/Zsh/zle.yo
@@ -2876,3 +2876,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 e5840ba3d..6257bd6c2 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";
static char *WAITVAR = ".term.querywait";
@@ -145,6 +144,7 @@ static char *WAITVAR = ".term.querywait";
* 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. */
@@ -427,32 +427,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,
@@ -467,12 +468,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 */
@@ -487,7 +488,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);
@@ -511,7 +512,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")))))
@@ -631,7 +632,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;
@@ -710,7 +711,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) */
@@ -761,3 +762,179 @@ 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) {
+ int s, 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;
+ int 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 20b4ab735..61aa03d0d 100644
--- a/Src/init.c
+++ b/Src/init.c
@@ -752,7 +752,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"
};
/**/
diff --git a/Src/zsh.h b/Src/zsh.h
index 53727f861..f233d5c8e 100644
--- a/Src/zsh.h
+++ b/Src/zsh.h
@@ -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])
Messages sorted by:
Reverse Date,
Date,
Thread,
Author