Skip to content

Commit e96ff19

Browse files
committed
Graphics protocol: Add support for having the terminal emulator assign image ids
Useful when multiple non co-operating programs want to share the screen. Fixes #3163
1 parent 09e75ea commit e96ff19

File tree

8 files changed

+214
-42
lines changed

8 files changed

+214
-42
lines changed

docs/changelog.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
1616
- Allow specifying text formatting in :opt:`tab_title_template` (:iss:`3146`)
1717

1818
- Graphics protocol: Add support for giving individual image placements their
19-
own ids. This is a backwards compatible protocol extension, and also allow
20-
suppressing responses from the terminal to commands (:iss:`3133`)
19+
own ids and for asking the terminal emulator to assign ids for images. Also
20+
allow suppressing responses from the terminal to commands.
21+
These are backwards compatible protocol extensions. (:iss:`3133`,
22+
:iss:`3163`)
2123

2224
- Distribute extra pixels among all eight-blocks rather than adding them
2325
all to the last block (:iss:`3097`)

docs/graphics-protocol.rst

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,8 @@ second one will replace the first. This can be used to resize or move
359359
placements around the screen, without flicker.
360360

361361

362-
.. note:: Support for specifying placement ids was added to kitty in
363-
versions after 0.19.2. You can use the protocol documented in the
364-
:doc:`kittens/query_terminal` to query kitty version.
362+
.. versionadded:: 0.19.3
363+
Support for specifying placement ids (see :doc:`kittens/query_terminal` to query kitty version)
365364

366365

367366
Controlling displayed image layout
@@ -415,7 +414,9 @@ Value of ``d`` Meaning
415414
================= ============
416415
``a`` or ``A`` Delete all placements visible on screen
417416
``i`` or ``I`` Delete all images with the specified id, specified using the ``i`` key. If you specify a ``p`` key for the placement id as well, then only the placement with the specified image id and placement id will be deleted.
418-
placement id
417+
``n`` or ``N`` Delete newest image with the specified number, specified using the ``I`` key. If you specify a ``p`` key for the
418+
placement id as well, then only the placement with the specified number and placement id will be deleted.
419+
``c`` or ``C`` Delete all placements that intersect with the current cursor position.
419420
``c`` or ``C`` Delete all placements that intersect with the current cursor position.
420421
``p`` or ``P`` Delete all placements that intersect a specific cell, the cell is specified using the ``x`` and ``y`` keys
421422
``q`` or ``Q`` Delete all placements that intersect a specific cell having a specific z-index. The cell and z-index is specified using the ``x``, ``y`` and ``z`` keys.
@@ -447,7 +448,40 @@ script, it might be useful to avoid having to process responses from the
447448
terminal. For this, you can use the ``q`` key. Set it to ``1`` to suppress
448449
``OK`` responses and to ``2`` to suppress failure responses.
449450

450-
.. note:: This feature was implemented in kitty in versions after 0.19.2
451+
.. versionadded:: 0.19.3
452+
The ability to suppress responses (see :doc:`kittens/query_terminal` to query kitty version)
453+
454+
455+
Requesting image ids from the terminal
456+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
457+
458+
If you are writing a program that is going to share the screen with other
459+
programs and you still want to use image ids, it is not possible to know
460+
what image ids are free to use. In this case, instead of using the ``i``
461+
key to specify and image id use the ``I`` key to specify and image number
462+
instead. These numbers are not unique.
463+
When creating a new image, even if an existing image has the same number a new
464+
one is created. And the terminal will reply with the id of the newly created
465+
image. For example, when creating an image with ``I=13``, the terminal will
466+
send the response::
467+
468+
<ESC>_Gi=99,I=13;OK<ESC>\
469+
470+
Here, the value of ``i`` is the id for the newly created image and the value of
471+
``I`` is the same as was sent in the creation command.
472+
473+
All future commands that refer to images using the image number, such as
474+
creating placements or deleting images, will act on only the newest image with
475+
that number. This allows the client program to send a bunch of commands dealing
476+
with an image by image number without waiting for a response from the terminal
477+
with the image id. Once such a response is received, the client program should
478+
use the ``i`` key with the image id for all future communication.
479+
480+
.. note:: Specifying both ``i`` and ``I`` keys in any command is an error. The
481+
terminal must reply with an EINVAL error message, unless silenced.
482+
483+
.. versionadded:: 0.19.3
484+
The ability to use image numbers (see :doc:`kittens/query_terminal` to query kitty version)
451485

452486

453487
Image persistence and storage quotas

gen-apc-parsers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def graphics_parser() -> None:
257257
'f': ('format', 'uint'),
258258
'm': ('more', 'uint'),
259259
'i': ('id', 'uint'),
260+
'I': ('image_number', 'uint'),
260261
'p': ('placement_id', 'uint'),
261262
'q': ('quiet', 'uint'),
262263
'w': ('width', 'uint'),

kitty/graphics.c

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ img_by_client_id(GraphicsManager *self, uint32_t id) {
9292
return NULL;
9393
}
9494

95+
static inline Image*
96+
img_by_client_number(GraphicsManager *self, uint32_t number) {
97+
// get the newest image with the specified number
98+
for (size_t i = self->image_count; i-- > 0; ) {
99+
if (self->images[i].client_number == number) return self->images + i;
100+
}
101+
return NULL;
102+
}
103+
104+
95105
static inline void
96106
remove_image(GraphicsManager *self, size_t idx) {
97107
free_image(self, self->images + idx);
@@ -306,6 +316,33 @@ find_or_create_image(GraphicsManager *self, uint32_t id, bool *existing) {
306316
return ans;
307317
}
308318

319+
static int
320+
cmp_client_ids(const void* a, const void* b) {
321+
const uint32_t *x = a, *y = b;
322+
return *x - *y;
323+
}
324+
325+
static inline uint32_t
326+
get_free_client_id(const GraphicsManager *self) {
327+
if (!self->image_count) return 1;
328+
uint32_t *client_ids = malloc(sizeof(uint32_t) * self->image_count);
329+
size_t count = 0;
330+
for (size_t i = 0; i < self->image_count; i++) {
331+
Image *q = self->images + i;
332+
if (q->client_id) client_ids[count++] = q->client_id;
333+
}
334+
if (!count) { free(client_ids); return 1; }
335+
qsort(client_ids, count, sizeof(uint32_t), cmp_client_ids);
336+
uint32_t prev_id = 0, ans = 1;
337+
for (size_t i = 0; i < count; i++) {
338+
if (client_ids[i] == prev_id) continue;
339+
prev_id = client_ids[i];
340+
if (client_ids[i] != ans) break;
341+
ans = client_ids[i] + 1;
342+
}
343+
free(client_ids);
344+
return ans;
345+
}
309346

310347
static Image*
311348
handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, bool *is_dirty, uint32_t iid) {
@@ -333,6 +370,11 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
333370
} else {
334371
img->internal_id = internal_id_counter++;
335372
img->client_id = iid;
373+
img->client_number = g->image_number;
374+
if (!img->client_id && img->client_number) {
375+
img->client_id = get_free_client_id(self);
376+
self->last_init_graphics_command.id = img->client_id;
377+
}
336378
}
337379
img->atime = monotonic(); img->used_storage = 0;
338380
img->width = g->data_width; img->height = g->data_height;
@@ -472,7 +514,7 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
472514
}
473515

474516
static inline const char*
475-
finish_command_response(const GraphicsCommand *g, bool data_loaded, uint32_t iid, uint32_t placement_id) {
517+
finish_command_response(const GraphicsCommand *g, bool data_loaded, uint32_t iid, uint32_t placement_id, uint32_t image_number) {
476518
static char rbuf[sizeof(command_response)/sizeof(command_response[0]) + 128];
477519
bool is_ok_response = !command_response[0];
478520
if (g->quiet) {
@@ -483,9 +525,14 @@ finish_command_response(const GraphicsCommand *g, bool data_loaded, uint32_t iid
483525
if (!data_loaded) return NULL;
484526
snprintf(command_response, 10, "OK");
485527
}
486-
if (placement_id) snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u,p=%u;%s", iid, placement_id, command_response);
487-
else snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u;%s", iid, command_response);
528+
size_t pos = 0;
529+
#define print(fmt, ...) pos += snprintf(rbuf + pos, arraysz(rbuf) - 1 - pos, fmt, __VA_ARGS__)
530+
print("Gi=%u", iid);
531+
if (image_number) print(",I=%u", image_number);
532+
if (placement_id) print(",p=%u", placement_id);
533+
print(";%s", command_response);
488534
return rbuf;
535+
#undef print
489536
}
490537
return NULL;
491538
}
@@ -521,11 +568,14 @@ update_dest_rect(ImageRef *ref, uint32_t num_cols, uint32_t num_rows, CellPixelS
521568
}
522569

523570

524-
static void
571+
static uint32_t
525572
handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img, CellPixelSize cell) {
526-
if (img == NULL) img = img_by_client_id(self, g->id);
527-
if (img == NULL) { set_command_failed_response("ENOENT", "Put command refers to non-existent image with id: %u", g->id); return; }
528-
if (!img->data_loaded) { set_command_failed_response("ENOENT", "Put command refers to image with id: %u that could not load its data", g->id); return; }
573+
if (img == NULL) {
574+
if (g->id) img = img_by_client_id(self, g->id);
575+
else if (g->image_number) img = img_by_client_number(self, g->image_number);
576+
if (img == NULL) { set_command_failed_response("ENOENT", "Put command refers to non-existent image with id: %u and number: %u", g->id, g->image_number); return 0; }
577+
}
578+
if (!img->data_loaded) { set_command_failed_response("ENOENT", "Put command refers to image with id: %u that could not load its data", g->id); return 0; }
529579
ensure_space_for(img, refs, ImageRef, img->refcnt + 1, refcap, 16, true);
530580
*is_dirty = true;
531581
self->layers_dirty = true;
@@ -556,6 +606,7 @@ handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, b
556606
update_dest_rect(ref, g->num_cells, g->num_lines, cell);
557607
// Move the cursor, the screen will take care of ensuring it is in bounds
558608
c->x += ref->effective_num_cols; c->y += ref->effective_num_rows - 1;
609+
return img->client_id;
559610
}
560611

561612
static int
@@ -649,20 +700,24 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree
649700
// Image lifetime/scrolling {{{
650701

651702
static inline void
652-
filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(const ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) {
703+
filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(const ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell, bool only_first_image) {
704+
bool matched = false;
653705
for (size_t i = self->image_count; i-- > 0;) {
654706
Image *img = self->images + i;
655707
for (size_t j = img->refcnt; j-- > 0;) {
656708
ImageRef *ref = img->refs + j;
657709
if (filter_func(ref, img, data, cell)) {
658710
remove_i_from_array(img->refs, j, img->refcnt);
659711
self->layers_dirty = true;
712+
matched = true;
660713
}
661714
}
662715
if (img->refcnt == 0 && (free_images || img->client_id == 0)) remove_image(self, i);
716+
if (only_first_image && matched) break;
663717
}
664718
}
665719

720+
666721
static inline void
667722
modify_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) {
668723
for (size_t i = self->image_count; i-- > 0;) {
@@ -743,7 +798,7 @@ clear_all_filter_func(const ImageRef *ref UNUSED, Image UNUSED *img, const void
743798

744799
void
745800
grman_clear(GraphicsManager *self, bool all, CellPixelSize cell) {
746-
filter_refs(self, NULL, true, all ? clear_all_filter_func : clear_filter_func, cell);
801+
filter_refs(self, NULL, true, all ? clear_all_filter_func : clear_filter_func, cell, false);
747802
}
748803

749804
static inline bool
@@ -753,6 +808,14 @@ id_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize
753808
return false;
754809
}
755810

811+
static inline bool
812+
number_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell UNUSED) {
813+
const GraphicsCommand *g = data;
814+
if (img->client_number == g->image_number) return !g->placement_id || ref->client_id == g->placement_id;
815+
return false;
816+
}
817+
818+
756819
static inline bool
757820
x_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) {
758821
const GraphicsCommand *g = data;
@@ -786,8 +849,9 @@ point3d_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixel
786849
static void
787850
handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, CellPixelSize cell) {
788851
static GraphicsCommand d;
852+
bool only_first_image = false;
789853
switch (g->delete_action) {
790-
#define I(u, data, func) filter_refs(self, data, g->delete_action == u, func, cell); *is_dirty = true; break
854+
#define I(u, data, func) filter_refs(self, data, g->delete_action == u, func, cell, only_first_image); *is_dirty = true; break
791855
#define D(l, u, data, func) case l: case u: I(u, data, func)
792856
#define G(l, u, func) D(l, u, g, func)
793857
case 0:
@@ -802,6 +866,10 @@ handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c
802866
case 'C':
803867
d.x_offset = c->x + 1; d.y_offset = c->y + 1;
804868
I('C', &d, point_filter_func);
869+
case 'n':
870+
case 'N':
871+
only_first_image = true;
872+
I('N', &g, number_filter_func);
805873
default:
806874
REPORT_ERROR("Unknown graphics command delete action: %c", g->delete_action);
807875
break;
@@ -839,6 +907,11 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint
839907
const char *ret = NULL;
840908
command_response[0] = 0;
841909

910+
if (g->id && g->image_number) {
911+
set_command_failed_response("EINVAL", "Must not specify both image id and image number");
912+
return finish_command_response(g, false, g->id, g->placement_id, g->image_number);
913+
}
914+
842915
switch(g->action) {
843916
case 0:
844917
case 't':
@@ -848,21 +921,21 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint
848921
bool is_query = g->action == 'q';
849922
if (is_query) { iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } }
850923
Image *image = handle_add_command(self, g, payload, is_dirty, iid);
851-
if (is_query) ret = finish_command_response(g, image != NULL, q_iid, 0);
852-
else ret = finish_command_response(g, image != NULL, self->last_init_graphics_command.id, self->last_init_graphics_command.placement_id);
924+
if (is_query) ret = finish_command_response(g, image != NULL, q_iid, 0, 0);
925+
else ret = finish_command_response(g, image != NULL, self->last_init_graphics_command.id, self->last_init_graphics_command.placement_id, self->last_init_graphics_command.image_number);
853926
if (self->last_init_graphics_command.action == 'T' && image && image->data_loaded) handle_put_command(self, &self->last_init_graphics_command, c, is_dirty, image, cell);
854927
id_type added_image_id = image ? image->internal_id : 0;
855928
if (g->action == 'q') remove_images(self, add_trim_predicate, 0);
856929
if (self->used_storage > STORAGE_LIMIT) apply_storage_quota(self, STORAGE_LIMIT, added_image_id);
857930
break;
858931
}
859932
case 'p':
860-
if (!g->id) {
861-
REPORT_ERROR("Put graphics command without image id");
933+
if (!g->id && !g->image_number) {
934+
REPORT_ERROR("Put graphics command without image id or number");
862935
break;
863936
}
864-
handle_put_command(self, g, c, is_dirty, NULL, cell);
865-
ret = finish_command_response(g, true, g->id, g->placement_id);
937+
uint32_t image_id = handle_put_command(self, g, c, is_dirty, NULL, cell);
938+
ret = finish_command_response(g, true, image_id, g->placement_id, g->image_number);
866939
break;
867940
case 'd':
868941
handle_delete_command(self, g, c, is_dirty, cell);
@@ -886,8 +959,8 @@ new(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
886959
static inline PyObject*
887960
image_as_dict(Image *img) {
888961
#define U(x) #x, img->x
889-
return Py_BuildValue("{sI sI sI sI sK sI sO sO sN}",
890-
U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt),
962+
return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sN}",
963+
U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number),
891964
"data_loaded", img->data_loaded ? Py_True : Py_False,
892965
"is_4byte_aligned", img->load_data.is_4byte_aligned ? Py_True : Py_False,
893966
"data", Py_BuildValue("y#", img->load_data.data, img->load_data.data_sz)
@@ -907,6 +980,13 @@ W(image_for_client_id) {
907980
return image_as_dict(img);
908981
}
909982

983+
W(image_for_client_number) {
984+
unsigned long num = PyLong_AsUnsignedLong(args);
985+
Image *img = img_by_client_number(self, num);
986+
if (!img) Py_RETURN_NONE;
987+
return image_as_dict(img);
988+
}
989+
910990
W(shm_write) {
911991
const char *name, *data;
912992
Py_ssize_t sz;
@@ -957,6 +1037,7 @@ W(update_layers) {
9571037

9581038
static PyMethodDef methods[] = {
9591039
M(image_for_client_id, METH_O),
1040+
M(image_for_client_number, METH_O),
9601041
M(update_layers, METH_VARARGS),
9611042
{NULL} /* Sentinel */
9621043
};

kitty/graphics.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
typedef struct {
1212
unsigned char action, transmission_type, compressed, delete_action;
13-
uint32_t format, more, id, data_sz, data_offset, placement_id, quiet;
13+
uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet;
1414
uint32_t width, height, x_offset, y_offset, data_height, data_width, num_cells, num_lines, cell_x_offset, cell_y_offset;
1515
int32_t z_index;
1616
size_t payload_sz;
@@ -44,7 +44,7 @@ typedef struct {
4444

4545

4646
typedef struct {
47-
uint32_t texture_id, client_id, width, height;
47+
uint32_t texture_id, client_id, client_number, width, height;
4848
id_type internal_id;
4949

5050
bool data_loaded;

0 commit comments

Comments
 (0)