Skip to content

Commit 8f5dc42

Browse files
committed
Parser for multi cursor escape code
1 parent e6c1597 commit 8f5dc42

File tree

5 files changed

+200
-7
lines changed

5 files changed

+200
-7
lines changed

kitty/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,14 @@ def multicell_command(payload: str) -> None:
299299
write(f'{OSC}{TEXT_SIZE_CODE};{m.rstrip(":")};{text}\a')
300300

301301

302+
def screen_multi_cursor(rest: str) -> None:
303+
write(f'{CSI}>{rest.strip()} q')
304+
305+
302306
def replay(raw: str) -> None:
303307
specials = frozenset({
304308
'draw', 'set_title', 'set_icon', 'set_dynamic_color', 'set_color_table_color', 'select_graphic_rendition',
305-
'process_cwd_notification', 'clipboard_control', 'shell_prompt_marking', 'multicell_command',
309+
'process_cwd_notification', 'clipboard_control', 'shell_prompt_marking', 'multicell_command', 'screen_multi_cursor',
306310
})
307311
for line in raw.splitlines():
308312
if line.strip() and not line.startswith('#'):

kitty/screen.c

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "char-props.h"
3232
#include "wcswidth.h"
3333
#include <stdalign.h>
34+
#include <stdio.h>
3435
#include "keys.h"
3536
#include "vt-parser.h"
3637
#include "resize.h"
@@ -2851,6 +2852,106 @@ screen_set_cursor(Screen *self, unsigned int mode, uint8_t secondary) {
28512852
}
28522853
}
28532854

2855+
#define NAME multi_cursor_map
2856+
#define KEY_TY index_type
2857+
#define VAL_TY uint8_t
2858+
#include "kitty-verstable.h"
2859+
2860+
void
2861+
screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_params) {
2862+
// printf("%d;", queried_shape); for (unsigned i = 0; i < num_params; i++) {printf("%d:", params[i]);} printf("\n");
2863+
if (!num_params) {
2864+
if (params == NULL) {
2865+
write_escape_code_to_child(self, ESC_CSI, ">-1;1;2;3 q");
2866+
} else if (queried_shape == -2) {
2867+
size_t sz = self->extra_cursors.count * 32 + 64;
2868+
RAII_ALLOC(char, buf, malloc(sz));
2869+
if (buf) {
2870+
char *p = buf + snprintf(buf, sz, ">-2;");
2871+
for (unsigned i = 0; i < self->extra_cursors.count; i++) {
2872+
index_type cell = self->extra_cursors.locations[i].cell, shape = self->extra_cursors.locations[i].shape;
2873+
index_type y = cell / self->columns, x = cell - (y * self->columns);
2874+
p += snprintf(p, sz - (p - buf), "%d:2:%u:%u;", shape > 3 ? -1 : (int)shape, y+1, x+1);
2875+
}
2876+
if (*(p-1) == ';') p--;
2877+
p += snprintf(p, sz - (p - buf), " q"); *p = 0;
2878+
write_escape_code_to_child(self, ESC_CSI, buf);
2879+
}
2880+
}
2881+
return;
2882+
}
2883+
uint8_t shape = 0;
2884+
if (queried_shape < 0) {
2885+
shape = 4;
2886+
} else {
2887+
shape = MIN(queried_shape, 3);
2888+
}
2889+
self->extra_cursors.dirty = true;
2890+
int type = params[0]; params++; num_params--;
2891+
switch (type) {
2892+
case 2: {
2893+
multi_cursor_map s; vt_init(&s);
2894+
for (unsigned i = 0; i < self->extra_cursors.count; i++) {
2895+
vt_insert(&s, self->extra_cursors.locations[i].cell, self->extra_cursors.locations[i].shape);
2896+
}
2897+
for (unsigned i = 0; i+1 < num_params; i+=2) {
2898+
index_type y = params[i]-1, x = params[i+1]-1;
2899+
if (!shape) { vt_erase(&s, y * self->columns + x); }
2900+
else if (y < self->lines && x < self->columns) vt_insert(&s, y * self->columns + x, shape);
2901+
}
2902+
self->extra_cursors.count = vt_size(&s);
2903+
ensure_space_for(&self->extra_cursors, locations, ExtraCursor, self->extra_cursors.count, capacity, 20 * 80, false);
2904+
self->extra_cursors.count = 0;
2905+
vt_create_for_loop(multi_cursor_map_itr, i, &s) {
2906+
self->extra_cursors.locations[self->extra_cursors.count++] = (ExtraCursor){
2907+
.shape = i.data->val, .cell = i.data->key};
2908+
}
2909+
vt_cleanup(&s);
2910+
} break;
2911+
case 4: {
2912+
if (!num_params) { // full screen
2913+
switch(shape) {
2914+
default: self->extra_cursors.count = 0; break;
2915+
case 1: case 2: case 3: case 4:
2916+
ensure_space_for(&self->extra_cursors, locations, ExtraCursor, self->lines * self->columns, capacity, 20 * 80, false);
2917+
self->extra_cursors.count = self->lines * self->columns;
2918+
for (index_type cell = 0; cell < self->lines * self->columns; cell++) {
2919+
self->extra_cursors.locations[cell].shape = shape;
2920+
self->extra_cursors.locations[cell].cell = cell;
2921+
}
2922+
break;
2923+
}
2924+
break;
2925+
}
2926+
unsigned count = 0;
2927+
for (unsigned i = 0; i < self->extra_cursors.count; i++) {
2928+
bool in_some_region = false;
2929+
index_type y = self->extra_cursors.locations[i].cell / self->columns, x = self->extra_cursors.locations[i].cell - (self->columns * y);
2930+
for (unsigned i = 0; i + 3 < num_params && !in_some_region; i += 4) {
2931+
index_type top = params[i]-1, left = params[i+1]-1, bottom = params[i+2]-1, right = params[i+3]-1;
2932+
in_some_region = top <= y && y <= bottom && left <= x && x <= right;
2933+
}
2934+
if (!in_some_region) self->extra_cursors.locations[count++] = self->extra_cursors.locations[i];
2935+
}
2936+
self->extra_cursors.count = count;
2937+
if (shape) {
2938+
for (unsigned i = 0; i + 3 < num_params; i += 4) {
2939+
index_type top = params[i]-1, left = params[i+1]-1, bottom = params[i+2]-1, right = params[i+3]-1;
2940+
index_type xnum = right + 1 - left, ynum = bottom + 1 - top;
2941+
ensure_space_for(&self->extra_cursors, locations, ExtraCursor,
2942+
self->extra_cursors.count + xnum * ynum, capacity, 20 * 80, false);
2943+
for (index_type y = top; y <= bottom; y++) {
2944+
for (index_type x = left; x <= right; x++) {
2945+
self->extra_cursors.locations[self->extra_cursors.count++] = (ExtraCursor){
2946+
.shape=shape, .cell=y*self->columns + x};
2947+
}
2948+
}
2949+
}
2950+
}
2951+
} break;
2952+
}
2953+
}
2954+
28542955
void
28552956
set_title(Screen *self, PyObject *title) {
28562957
CALLBACK("title_changed", "O", title);

kitty/screen.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ bool parse_sgr(Screen *screen, const uint8_t *buf, unsigned int num, const char
305305
bool screen_pause_rendering(Screen *self, bool pause, int for_in_ms);
306306
void screen_check_pause_rendering(Screen *self, monotonic_t now);
307307
void screen_designate_charset(Screen *self, uint32_t which, uint32_t as);
308+
void screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_params);
308309
#define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen);
309310
DECLARE_CH_SCREEN_HANDLER(bell)
310311
DECLARE_CH_SCREEN_HANDLER(backspace)

kitty/vt-parser.c

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ _report_params(PyObject *dump_callback, id_type window_id, const char *name, int
7272
Py_XDECREF(PyObject_CallFunction(dump_callback, "Kss", window_id, name, buf)); PyErr_Clear();
7373
}
7474

75+
static void
76+
_report_params_with_first(PyObject *dump_callback, id_type window_id, const char *name, int first_param, int *params, unsigned count) {
77+
static char buf[MAX_CSI_PARAMS*3] = {0};
78+
unsigned int i, p=0;
79+
p += snprintf(buf + p, sizeof(buf) - 2, "%d;", first_param);
80+
for(i = 0; i < count && p < arraysz(buf)-20; i++) {
81+
int n = snprintf(buf + p, arraysz(buf) - p, "%i:", params[i]);
82+
if (n < 0) break;
83+
p += n;
84+
}
85+
buf[count ? p-1 : p] = 0;
86+
Py_XDECREF(PyObject_CallFunction(dump_callback, "Kss", window_id, name, buf)); PyErr_Clear();
87+
}
88+
89+
7590
#define DUMP_UNUSED
7691

7792
#define REPORT_ERROR(...) _report_error(self->dump_callback, self->window_id, __VA_ARGS__);
@@ -110,7 +125,9 @@ _report_params(PyObject *dump_callback, id_type window_id, const char *name, int
110125
}
111126

112127

113-
#define REPORT_PARAMS(name, params, num, is_group, region) _report_params(self->dump_callback, self->window_id, name, params, num_params, is_group, region)
128+
#define REPORT_PARAMS(name, params, num, is_group, region) _report_params(self->dump_callback, self->window_id, name, params, num, is_group, region)
129+
130+
#define REPORT_PARAMS_WITH_FIRST(name, first, params, num) _report_params_with_first(self->dump_callback, self->window_id, name, first, params, num)
114131

115132
#define REPORT_OSC(name, string) \
116133
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "KsO", self->window_id, #name, string)); PyErr_Clear();
@@ -127,6 +144,7 @@ _report_params(PyObject *dump_callback, id_type window_id, const char *name, int
127144
#define REPORT_VA_COMMAND(...)
128145
#define REPORT_DRAW(...)
129146
#define REPORT_PARAMS(...)
147+
#define REPORT_PARAMS_WITH_FIRST(...)
130148
#define REPORT_OSC(name, string)
131149
#define REPORT_OSC2(name, code, string)
132150
#define REPORT_HYPERLINK(id, url)
@@ -875,6 +893,35 @@ consume_csi(PS *self) {
875893
return csi_parse_loop(self, &self->csi, self->buf, &self->read.pos, self->read.sz, self->read.consumed);
876894
}
877895

896+
static void
897+
_parse_multi_cursors(PS *self, ParsedCSI *csi) {
898+
switch(csi->num_params) {
899+
case 0:
900+
REPORT_COMMAND("screen_multi_cursor");
901+
screen_multi_cursor(self->screen, 0, NULL, 0);
902+
break;
903+
case 1:
904+
REPORT_PARAMS_WITH_FIRST("screen_multi_cursor", csi->params[0], csi->params, 0);
905+
screen_multi_cursor(self->screen, csi->params[0], csi->params, 0);
906+
break;
907+
default: {
908+
unsigned pos = 1, first_param = pos;
909+
for (; pos < csi->num_params; pos++) {
910+
if (pos > first_param) {
911+
if (!csi->is_sub_param[pos]) {
912+
REPORT_PARAMS_WITH_FIRST("screen_multi_cursor", csi->params[0], csi->params + first_param, pos - first_param);
913+
screen_multi_cursor(self->screen, csi->params[0], csi->params + first_param, pos - first_param);
914+
first_param = pos;
915+
}
916+
}
917+
}
918+
if (pos > first_param) {
919+
REPORT_PARAMS_WITH_FIRST("screen_multi_cursor", csi->params[0], csi->params + first_param, pos - first_param);
920+
screen_multi_cursor(self->screen, csi->params[0], csi->params + first_param, pos - first_param);
921+
}}}
922+
}
923+
924+
878925
static unsigned int
879926
parse_region(const ParsedCSI *csi, Region *r) {
880927
switch(csi->num_params) {
@@ -895,7 +942,6 @@ parse_region(const ParsedCSI *csi, Region *r) {
895942
}
896943
}
897944

898-
899945
static bool
900946
_parse_sgr(PS *self, ParsedCSI *csi) {
901947
#define SEND_SGR if (num_params) { \
@@ -1297,10 +1343,13 @@ dispatch_csi(PS *self) {
12971343
REPORT_ERROR("Unknown CSI x sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
12981344
break;
12991345
case DECSCUSR:
1300-
if (!start_modifier && end_modifier == ' ') {
1301-
CALL_CSI_HANDLER1M(screen_set_cursor, 1);
1302-
}
1303-
if (start_modifier == '>' && !end_modifier) {
1346+
if (end_modifier == ' ') {
1347+
if (!start_modifier) { CALL_CSI_HANDLER1M(screen_set_cursor, 1); }
1348+
if (start_modifier == '>') {
1349+
_parse_multi_cursors(self, &self->csi);
1350+
break;
1351+
}
1352+
} else if (end_modifier == 0 && start_modifier == '>') {
13041353
CALL_CSI_HANDLER1(screen_xtversion, 0);
13051354
}
13061355
REPORT_ERROR("Unknown CSI q sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);

kitty_tests/screen.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,44 @@ def q(send, expected=None):
15891589
q({'transparent_background_color2': '#ffffff@-1'})
15901590
q({'transparent_background_color2': '?'}, {'transparent_background_color2': (Color(255, 255, 255), 255)})
15911591

1592+
def test_multi_cursors(self):
1593+
s = self.create_screen()
1594+
c = s.callbacks
1595+
# Test detection
1596+
parse_bytes(s, b'\x1b[> q') # ]
1597+
self.ae(c.wtcbuf, b'\x1b[>-1;1;2;3 q') # ]
1598+
1599+
def current() -> dict[int, tuple[int, int]]:
1600+
ans = {}
1601+
c.clear()
1602+
parse_bytes(s, '\x1b[>-2 q'.encode()) # ]
1603+
for entry in c.wtcbuf[6:-2].decode().split(';'):
1604+
if entry:
1605+
which, _, y, x = map(int, entry.split(':'))
1606+
ans.setdefault(which, set()).add((x-1, y-1))
1607+
return ans
1608+
self.ae({}, current())
1609+
1610+
def a(which: int, *positions: tuple[int, int], region=None) -> dict[int, tuple[int, int]]:
1611+
if positions:
1612+
buf = [f'\x1b[>{which};'] # ]
1613+
buf.extend(f'2:{y+1}:{x+1};' for x, y in positions)
1614+
parse_bytes(s, ''.join(buf).encode() + b' q')
1615+
if region:
1616+
if region is True:
1617+
parse_bytes(s, f'\x1b[>{which};4 q'.encode()) # ]
1618+
else:
1619+
left, top, right, bottom = region
1620+
parse_bytes(s, f'\x1b[>{which};4:{top+1}:{left+1}:{bottom+1}:{right+1} q'.encode()) # ]
1621+
return current()
1622+
1623+
self.ae(a(1, region=True), {1:{(x, y) for x in range(s.columns) for y in range(s.lines)}})
1624+
self.ae(a(0, region=True), {})
1625+
self.ae(a(-1, region=(1, 2, 2, 3)), {-1: {(1, 2), (2, 2), (1, 3), (2, 3)}})
1626+
self.ae(a(2, (1, 2), (1, 3)), {-1: {(2, 3), (2, 2)}, 2: {(1, 2), (1, 3)}})
1627+
self.ae(a(0, (1, 2), (2, 3)), {-1: {(2, 2)}, 2: {(1, 3)}})
1628+
self.ae(a(0, region=True), {})
1629+
15921630

15931631
def detect_url(self, scale=1):
15941632
s = self.create_screen(cols=30 * scale)

0 commit comments

Comments
 (0)