Dont use negative numbers in multi cursor protocol

There are apparently many parsers out there that cant handle them.
This commit is contained in:
Kovid Goyal 2025-08-26 21:43:37 +05:30
parent e76f4c630f
commit 44b5d8f656
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 30 additions and 28 deletions

View file

@ -25,7 +25,7 @@ An example, showing how to use the protocol:
.. code-block:: sh
# Show cursors of the same shape as the main cursor at y=4, x=5
printf "\e[>-1;2:4:5 q"
printf "\e[>29;2:4:5 q"
# Show more cursors on the seventh line, of various shapes, the underline shape is shown twice
printf "\e[>1;2:7:1 q\e[>2;2:7:3 q\e[>3;2:7:5;2:7:7 q"
@ -38,12 +38,14 @@ they are present for readability only)::
Here ``CSI`` is the two bytes ESC (``0x1b``) and [ (``0x5b``). ``SHAPE`` can be
one of:
* ``-2``: Used for querying currently set cursors
* ``-1``: Follow the shape of the main cursor
* ``0``: No cursor
* ``1``: Block cursor
* ``2``: Beam cursor
* ``3``: Underline cursor
* ``29``: Follow the shape of the main cursor
* ``30``: Change the color of text under extra cursors
* ``40``: Change the color of extra cursors
* ``100``: Used for querying currently set cursors
``CO-ORD TYPE`` can be one of:
@ -92,7 +94,7 @@ protocol by sending the escape code::
In this case a supporting terminal must reply with::
CSI > -2;-1;1;2;3 TRAILER
CSI > 1;2;3;29;30;40;100;101 TRAILER
Here, the list of numbers indicates the cursor shapes and other operations
the terminal supports and can be any subset of the above. No numbers
@ -125,18 +127,18 @@ Querying for already set cursors
Programs can ask the terminal what extra cursors are currently set, by sending
the escape code::
CSI > -2 TRAILER
CSI > 100 TRAILER
The terminal must respond with **one** escape code::
CSI > -2; SHAPE:CO-ORDINATE TYPE:CO-ORDINATES ; ... TRAILER
CSI > 100; SHAPE:CO-ORDINATE TYPE:CO-ORDINATES ; ... TRAILER
Here, the ``SHAPE:CO-ORDINATE TYPE:CO-ORDINATES`` block can be repeated any
number of times, separated by ``;``. This response gives the set of shapes and
positions currently active. If no cursors are currently active, there will be
no blocks, just an empty response of the form::
CSI > -2 TRAILER
CSI > 100 TRAILER
Again, terminals **must** respond in FIFO order so that multiplexers know where
to direct the responses.

View file

@ -2868,24 +2868,24 @@ screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_p
if (!num_params) {
#define pr(...) { int n = snprintf(p, sz - (p - buf), __VA_ARGS__); if (n >= 0 && (unsigned)n <= (sz - (p - buf))) p += n; }
if (params == NULL) {
write_escape_code_to_child(self, ESC_CSI, ">-5;-4;-3;-2;-1;1;2;3 q");
} else if (queried_shape == -2) {
write_escape_code_to_child(self, ESC_CSI, ">1;2;3;29;30;40;100;101 q");
} else if (queried_shape == 100) {
size_t sz = self->extra_cursors.count * 32 + 64;
RAII_ALLOC(char, buf, malloc(sz)); sz -= 4;
if (buf) {
char *p = buf + snprintf(buf, sz, ">-2;");
char *p = buf + snprintf(buf, sz, ">100;");
for (unsigned i = 0; i < self->extra_cursors.count; i++) {
index_type cell = self->extra_cursors.locations[i].cell, shape = self->extra_cursors.locations[i].shape;
index_type y = cell / self->columns, x = cell - (y * self->columns);
pr("%d:2:%u:%u;", shape > 3 ? -1 : (int)shape, y+1, x+1);
pr("%d:2:%u:%u;", shape > 3 ? 29 : (int)shape, y+1, x+1);
}
if (*(p-1) == ';') p--;
*(p++) = ' '; *(p++) = 'q'; *(p++) = 0;
write_escape_code_to_child(self, ESC_CSI, buf);
}
} else if (queried_shape == -5) {
} else if (queried_shape == 101) {
char buf[64], *p = buf; size_t sz = sizeof(buf);
pr(">-5;-3:"); DynamicColor ecc = self->extra_cursors.color.cursor;
pr(">101;30:"); DynamicColor ecc = self->extra_cursors.color.text;
#define o() switch(ecc.type) { \
case COLOR_NOT_SET: pr("0"); break; \
case COLOR_IS_SPECIAL: pr("1"); break; \
@ -2893,7 +2893,7 @@ screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_p
case COLOR_IS_RGB: pr("2:%u:%u:%u", (ecc.rgb >> 16) & 0xff, (ecc.rgb >> 8) & 0xff, ecc.rgb & 0xff); break; \
} \
o(); pr(";-4:"); ecc = self->extra_cursors.color.text; o();
o(); pr(";40:"); ecc = self->extra_cursors.color.cursor; o();
#undef o
pr(" q");
write_escape_code_to_child(self, ESC_CSI, buf);
@ -2901,8 +2901,8 @@ screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_p
return;
#undef pr
}
if (queried_shape == -3 || queried_shape == -4) {
DynamicColor *ecc = queried_shape == -3 ? &self->extra_cursors.color.cursor : &self->extra_cursors.color.text;
if (queried_shape == 30 || queried_shape == 40) {
DynamicColor *ecc = queried_shape == 40 ? &self->extra_cursors.color.cursor : &self->extra_cursors.color.text;
self->extra_cursors.dirty = true;
switch (params[0]) {
case 0: ecc->type = COLOR_NOT_SET; break;
@ -2919,10 +2919,10 @@ screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_p
return;
}
uint8_t shape = 0;
if (queried_shape < 0) {
shape = 4;
} else {
shape = MIN(queried_shape, 3);
switch(queried_shape) {
case 29: shape = 4; break;
case 0: case 1: case 2: case 3: shape = queried_shape; break;
default: return;
}
self->extra_cursors.dirty = true;
int type = params[0]; params++; num_params--;

View file

@ -1597,12 +1597,12 @@ class TestScreen(BaseTest):
def ec(payload=''):
return f'\x1b[>{payload} q'.encode() # ]
parse_bytes(s, ec())
self.ae(c.wtcbuf, ec('-5;-4;-3;-2;-1;1;2;3'))
self.ae(c.wtcbuf, ec('1;2;3;29;30;40;100;101'))
def current() -> dict[int, tuple[int, int]]:
ans = {}
c.clear()
parse_bytes(s, ec('-2'))
parse_bytes(s, ec('100'))
for entry in c.wtcbuf[6:-2].decode().split(';'):
if entry:
which, _, y, x = map(int, entry.split(':'))
@ -1625,9 +1625,9 @@ class TestScreen(BaseTest):
self.ae(a(1, region=True), {1:{(x, y) for x in range(s.columns) for y in range(s.lines)}})
self.ae(a(0, region=True), {})
self.ae(a(-1, region=(1, 2, 2, 3)), {-1: {(1, 2), (2, 2), (1, 3), (2, 3)}})
self.ae(a(2, (1, 2), (1, 3)), {-1: {(2, 3), (2, 2)}, 2: {(1, 2), (1, 3)}})
self.ae(a(0, (1, 2), (2, 3)), {-1: {(2, 2)}, 2: {(1, 3)}})
self.ae(a(29, region=(1, 2, 2, 3)), {29: {(1, 2), (2, 2), (1, 3), (2, 3)}})
self.ae(a(2, (1, 2), (1, 3)), {29: {(2, 3), (2, 2)}, 2: {(1, 2), (1, 3)}})
self.ae(a(0, (1, 2), (2, 3)), {29: {(2, 2)}, 2: {(1, 3)}})
self.ae(a(0, region=True), {})
s.cursor.x, s.cursor.y = 1, 2
parse_bytes(s, ec('3;0'))
@ -1637,10 +1637,10 @@ class TestScreen(BaseTest):
parse_bytes(s, ec('0;4:3:1:4'))
self.ae(current(), {})
def sc(op, r=0, g=0, b=0, slot=-3):
def sc(op, r=0, g=0, b=0, slot=40):
parse_bytes(s, ec(f'{slot};{op}:{r}:{g}:{b}'))
c.clear()
parse_bytes(s, ec('-5'))
parse_bytes(s, ec('101'))
for x in c.wtcbuf[3:-2].decode().split(';')[1:]:
parts = x.split(':')
if int(parts[0]) == slot:
@ -1651,7 +1651,7 @@ class TestScreen(BaseTest):
else:
self.ae((op, r), tuple(map(int, parts[1:])))
break
for slot in (-3, -4):
for slot in (40, 30):
sc(0, slot=slot)
sc(1, slot=slot)
sc(2, 1, 2, 3, slot=slot)