From 1c9a2dae86e5fd9db4dfc86088e9284f94783251 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Aug 2025 19:59:00 +0530 Subject: [PATCH] Start work on documenting the multi cursor protocol --- docs/multiple-cursors-protocol.rst | 150 +++++++++++++++++++++++++++++ docs/protocol-extensions.rst | 1 + kitty/screen.c | 19 ++-- 3 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 docs/multiple-cursors-protocol.rst diff --git a/docs/multiple-cursors-protocol.rst b/docs/multiple-cursors-protocol.rst new file mode 100644 index 000000000..af3c20cef --- /dev/null +++ b/docs/multiple-cursors-protocol.rst @@ -0,0 +1,150 @@ +The multiple cursors protocol +============================================== + +.. versionadded:: 0.43.0 + +Many editors support something called *multiple cursors* in which you can make +the same changes at multiple locations in a file and the editor shows you +cursors at each of the locations. In a terminal context editors typically +implement this by showing some Unicode glyph at each location instead of the +actual cursor. This is sub-optimal since actual cursors implemented by the +terminal have many niceties like smooth animation, auto adjust colors, etc. To +address this and other use cases, this protocol allows terminal programs to +request that the terminal display multiple cursors at specific locations on the +screen. + +Quickstart +---------------- + +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" + # 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" + + +The escape code to show a cursor has the following structure (ignore spaces +they are present for readability only):: + + CSI > SHAPE;CO-ORD TYPE : CO-ORDINATES ; CO-ORD TYPE : CO-ORDINATES ... TRAILER + +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 + +``CO-ORD TYPE`` can be one of: + +* ``0``: This refers to the position of the main cursor and has no following + co-ordinates. + +* ``2``: In this case the following co-ordinates are pairs of numbers pointing + to cells in the form ``y:x`` with the origin in the top left corner at + ``1,1``. There can be any number of pairs, the terminal must treat each pair + as a new location to set a cursor. + +* ``4``: In this case the following co-ordinates are sets of four numbers that + define a rectangle in the same co-ordinate system as above of the form: + ``top:left:bottom:right``. The shape is set on every cell in the rectangle + from the top left cell to the bottom right cell, inclusive. If no numbers + are provided, the rectangle is the full screen. There can be any number of + rectangles, the terminal must treat each set of four numbers as a new + rectangle. + +The sequence of ``CO-ORD TYPE : CO-ORDINATES`` can be repeated any number of +times separated by ``;``. The ``SHAPE`` will be set on the cells indicated by +each such group. For example: ``-1;2:3:4;4:5:6:7:8`` will set the shape ``-1`` +at the cell ``(3, 2)`` and in the rectangle ``(6, 5)`` to ``(8, 7)`` inclusive. + +Finally, the ``TRAILER`` terminates the sequence and is the bytes SPACE +(``0x20``) and q (``0x71``). + +Terminals **must** ignore cells that fall outside the screen. That means, for +rectangle co-ordinates only the intersection of the rectangle with the screen +must be considered, and point co-ordinates that fall outside of the screen are +simply ignored, with no effect. + +Terminals **must** ignore extra co-ordinates, that means if an odd number of +co-ordinates are specified for type ``2`` the last co-ordinate is ignored. +Similarly for type ``4`` if the number of co-ordinates is not a multiple of +four, the last ``1 <= n <= 3`` co-ordinates are ignored, as if they were not +specified. + +Querying for support +------------------------- + +A terminal program can query the terminal emulator for support of this +protocol by sending the escape code:: + + CSI > TRAILER + +In this case a supporting terminal must reply with:: + + CSI > -1;1;2;3 TRAILER + +Here, the list of numbers indicates the cursor shapes the terminal supports and +can be any subset of the above. No numbers indicates the protocol is not +supported. To avoid having to wait with a timeout for a response from the +terminal, the client should send this query code immediately followed by +a request for the `primary device attributes `_. +If the terminal responds with an answer for the device attributes without +an answer for the *query* the terminal emulator does not support this protocol at all. + +Terminals **must** respond to these queries in FIFO order, so that +multiplexers that split a single screen know which split to send responses too. + +Clearing previously set multi-cursors +------------------------------------------ + +The cursor at a cell is cleared by settings its shape to ``0``. +The most common operation is to clear all previously set multi-cursors. This is +easily done using the *rectangle* co-ordinate system above, like this:: + + CSI > 0;4 TRAILER + +For more precise control different co-ordinate types can be used. This is +particularly important for multiplexers that split up the screen and therefore +need to re-write these escape codes. + +Querying for already set cursors +-------------------------------------- + +Programs can ask the terminal what extra cursors are currently set, by sending +the escape code:: + + CSI > -2 TRAILER + +The terminal must respond with **one** escape code:: + + CSI > -2; 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 + +Again, terminals **must** respond in FIFO order so that multiplexers know where +to direct the responses. + + +Interaction with other terminal controls and state +------------------------------------------------------- + +**The main cursor** + The extra cursors must all have the same color and opacity and blink state + as the main cursor. The main cursor's visibility must not affect the + visibility of the extra cursors. Their visibility and shape are controlled + only by this protocol. + +**Clearing the screen** + diff --git a/docs/protocol-extensions.rst b/docs/protocol-extensions.rst index 68c3d34fd..0cb0778b0 100644 --- a/docs/protocol-extensions.rst +++ b/docs/protocol-extensions.rst @@ -28,6 +28,7 @@ please do so by opening issues in the `GitHub bug tracker graphics-protocol keyboard-protocol text-sizing-protocol + multiple-cursors-protocol file-transfer-protocol desktop-notifications pointer-shapes diff --git a/kitty/screen.c b/kitty/screen.c index 61b2321af..b9dda8e3a 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1900,11 +1900,6 @@ screen_is_cursor_visible(const Screen *self) { return self->paused_rendering.expires_at ? self->paused_rendering.cursor_visible : self->modes.mDECTCEM; } -unsigned -screen_multi_cursor_count(const Screen *self) { - return self->paused_rendering.expires_at ? self->paused_rendering.extra_cursors.count : self->extra_cursors.count; -} - void screen_backspace(Screen *self) { screen_cursor_move(self, 1, -1, true); @@ -2862,6 +2857,11 @@ screen_set_cursor(Screen *self, unsigned int mode, uint8_t secondary) { #define VAL_TY uint8_t #include "kitty-verstable.h" +unsigned +screen_multi_cursor_count(const Screen *self) { + return self->paused_rendering.expires_at ? self->paused_rendering.extra_cursors.count : self->extra_cursors.count; +} + void screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_params) { // printf("%d;", queried_shape); for (unsigned i = 0; i < num_params; i++) {printf("%d:", params[i]);} printf("\n"); @@ -2893,7 +2893,13 @@ screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_p } self->extra_cursors.dirty = true; int type = params[0]; params++; num_params--; + int extra[2]; switch (type) { + case 0: + extra[0] = MIN(self->cursor->y, self->lines-1) + 1; + extra[1] = MIN(self->cursor->x, self->columns-1) + 1; + params = extra; num_params = 2; + /* fallthrough */ case 2: { multi_cursor_map s; vt_init(&s); for (unsigned i = 0; i < self->extra_cursors.count; i++) { @@ -2914,7 +2920,7 @@ screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_p vt_cleanup(&s); } break; case 4: { - if (!num_params) { // full screen + if (num_params < 4) { // full screen switch(shape) { default: self->extra_cursors.count = 0; break; case 1: case 2: case 3: case 4: @@ -2942,6 +2948,7 @@ screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_p if (shape) { for (unsigned i = 0; i + 3 < num_params; i += 4) { index_type top = params[i]-1, left = params[i+1]-1, bottom = params[i+2]-1, right = params[i+3]-1; + bottom = MIN(bottom, self->lines-1); right = MIN(right, self->columns -1); index_type xnum = right + 1 - left, ynum = bottom + 1 - top; ensure_space_for(&self->extra_cursors, locations, ExtraCursor, self->extra_cursors.count + xnum * ynum, capacity, 20 * 80, false);