From 44b5d8f656e1d579c69b82df8cbd98c75fc9aa77 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Aug 2025 21:43:37 +0530 Subject: [PATCH] Dont use negative numbers in multi cursor protocol There are apparently many parsers out there that cant handle them. --- docs/multiple-cursors-protocol.rst | 16 +++++++++------- kitty/screen.c | 26 +++++++++++++------------- kitty_tests/screen.py | 16 ++++++++-------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/docs/multiple-cursors-protocol.rst b/docs/multiple-cursors-protocol.rst index 2a69ecd6c..1bebc38e5 100644 --- a/docs/multiple-cursors-protocol.rst +++ b/docs/multiple-cursors-protocol.rst @@ -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. diff --git a/kitty/screen.c b/kitty/screen.c index c252dc2d8..f2d3e774d 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -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--; diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 36aef7e0c..f3482ea43 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -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)