From e96ff19a7ac747297b08364f42bb9a314e48c77e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 16 Dec 2020 17:31:15 +0530 Subject: [PATCH] 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 --- docs/changelog.rst | 6 +- docs/graphics-protocol.rst | 44 +++++++++++-- gen-apc-parsers.py | 1 + kitty/graphics.c | 117 ++++++++++++++++++++++++++++----- kitty/graphics.h | 4 +- kitty/parse-graphics-command.h | 34 ++++++---- kitty_tests/graphics.py | 48 ++++++++++++++ kitty_tests/parser.py | 2 +- 8 files changed, 214 insertions(+), 42 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 84bee5e45..01f43a77d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,8 +16,10 @@ To update |kitty|, :doc:`follow the instructions `. - Allow specifying text formatting in :opt:`tab_title_template` (:iss:`3146`) - Graphics protocol: Add support for giving individual image placements their - own ids. This is a backwards compatible protocol extension, and also allow - suppressing responses from the terminal to commands (:iss:`3133`) + own ids and for asking the terminal emulator to assign ids for images. Also + allow suppressing responses from the terminal to commands. + These are backwards compatible protocol extensions. (:iss:`3133`, + :iss:`3163`) - Distribute extra pixels among all eight-blocks rather than adding them all to the last block (:iss:`3097`) diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index 767772be5..c15ef8aff 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -359,9 +359,8 @@ second one will replace the first. This can be used to resize or move placements around the screen, without flicker. -.. note:: Support for specifying placement ids was added to kitty in - versions after 0.19.2. You can use the protocol documented in the - :doc:`kittens/query_terminal` to query kitty version. +.. versionadded:: 0.19.3 + Support for specifying placement ids (see :doc:`kittens/query_terminal` to query kitty version) Controlling displayed image layout @@ -415,7 +414,9 @@ Value of ``d`` Meaning ================= ============ ``a`` or ``A`` Delete all placements visible on screen ``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. - placement id +``n`` or ``N`` Delete newest image with the specified number, specified using the ``I`` key. If you specify a ``p`` key for the + placement id as well, then only the placement with the specified number and placement id will be deleted. +``c`` or ``C`` Delete all placements that intersect with the current cursor position. ``c`` or ``C`` Delete all placements that intersect with the current cursor position. ``p`` or ``P`` Delete all placements that intersect a specific cell, the cell is specified using the ``x`` and ``y`` keys ``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 terminal. For this, you can use the ``q`` key. Set it to ``1`` to suppress ``OK`` responses and to ``2`` to suppress failure responses. -.. note:: This feature was implemented in kitty in versions after 0.19.2 +.. versionadded:: 0.19.3 + The ability to suppress responses (see :doc:`kittens/query_terminal` to query kitty version) + + +Requesting image ids from the terminal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are writing a program that is going to share the screen with other +programs and you still want to use image ids, it is not possible to know +what image ids are free to use. In this case, instead of using the ``i`` +key to specify and image id use the ``I`` key to specify and image number +instead. These numbers are not unique. +When creating a new image, even if an existing image has the same number a new +one is created. And the terminal will reply with the id of the newly created +image. For example, when creating an image with ``I=13``, the terminal will +send the response:: + + _Gi=99,I=13;OK\ + +Here, the value of ``i`` is the id for the newly created image and the value of +``I`` is the same as was sent in the creation command. + +All future commands that refer to images using the image number, such as +creating placements or deleting images, will act on only the newest image with +that number. This allows the client program to send a bunch of commands dealing +with an image by image number without waiting for a response from the terminal +with the image id. Once such a response is received, the client program should +use the ``i`` key with the image id for all future communication. + +.. note:: Specifying both ``i`` and ``I`` keys in any command is an error. The + terminal must reply with an EINVAL error message, unless silenced. + +.. versionadded:: 0.19.3 + The ability to use image numbers (see :doc:`kittens/query_terminal` to query kitty version) Image persistence and storage quotas diff --git a/gen-apc-parsers.py b/gen-apc-parsers.py index 3e42d9e88..6fe2c4a8e 100755 --- a/gen-apc-parsers.py +++ b/gen-apc-parsers.py @@ -257,6 +257,7 @@ def graphics_parser() -> None: 'f': ('format', 'uint'), 'm': ('more', 'uint'), 'i': ('id', 'uint'), + 'I': ('image_number', 'uint'), 'p': ('placement_id', 'uint'), 'q': ('quiet', 'uint'), 'w': ('width', 'uint'), diff --git a/kitty/graphics.c b/kitty/graphics.c index 05f4144e9..fb6ee25dd 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -92,6 +92,16 @@ img_by_client_id(GraphicsManager *self, uint32_t id) { return NULL; } +static inline Image* +img_by_client_number(GraphicsManager *self, uint32_t number) { + // get the newest image with the specified number + for (size_t i = self->image_count; i-- > 0; ) { + if (self->images[i].client_number == number) return self->images + i; + } + return NULL; +} + + static inline void remove_image(GraphicsManager *self, size_t idx) { free_image(self, self->images + idx); @@ -306,6 +316,33 @@ find_or_create_image(GraphicsManager *self, uint32_t id, bool *existing) { return ans; } +static int +cmp_client_ids(const void* a, const void* b) { + const uint32_t *x = a, *y = b; + return *x - *y; +} + +static inline uint32_t +get_free_client_id(const GraphicsManager *self) { + if (!self->image_count) return 1; + uint32_t *client_ids = malloc(sizeof(uint32_t) * self->image_count); + size_t count = 0; + for (size_t i = 0; i < self->image_count; i++) { + Image *q = self->images + i; + if (q->client_id) client_ids[count++] = q->client_id; + } + if (!count) { free(client_ids); return 1; } + qsort(client_ids, count, sizeof(uint32_t), cmp_client_ids); + uint32_t prev_id = 0, ans = 1; + for (size_t i = 0; i < count; i++) { + if (client_ids[i] == prev_id) continue; + prev_id = client_ids[i]; + if (client_ids[i] != ans) break; + ans = client_ids[i] + 1; + } + free(client_ids); + return ans; +} static Image* 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_ } else { img->internal_id = internal_id_counter++; img->client_id = iid; + img->client_number = g->image_number; + if (!img->client_id && img->client_number) { + img->client_id = get_free_client_id(self); + self->last_init_graphics_command.id = img->client_id; + } } img->atime = monotonic(); img->used_storage = 0; img->width = g->data_width; img->height = g->data_height; @@ -472,7 +514,7 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ } static inline const char* -finish_command_response(const GraphicsCommand *g, bool data_loaded, uint32_t iid, uint32_t placement_id) { +finish_command_response(const GraphicsCommand *g, bool data_loaded, uint32_t iid, uint32_t placement_id, uint32_t image_number) { static char rbuf[sizeof(command_response)/sizeof(command_response[0]) + 128]; bool is_ok_response = !command_response[0]; if (g->quiet) { @@ -483,9 +525,14 @@ finish_command_response(const GraphicsCommand *g, bool data_loaded, uint32_t iid if (!data_loaded) return NULL; snprintf(command_response, 10, "OK"); } - if (placement_id) snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u,p=%u;%s", iid, placement_id, command_response); - else snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u;%s", iid, command_response); + size_t pos = 0; +#define print(fmt, ...) pos += snprintf(rbuf + pos, arraysz(rbuf) - 1 - pos, fmt, __VA_ARGS__) + print("Gi=%u", iid); + if (image_number) print(",I=%u", image_number); + if (placement_id) print(",p=%u", placement_id); + print(";%s", command_response); return rbuf; +#undef print } return NULL; } @@ -521,11 +568,14 @@ update_dest_rect(ImageRef *ref, uint32_t num_cols, uint32_t num_rows, CellPixelS } -static void +static uint32_t handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img, CellPixelSize cell) { - if (img == NULL) img = img_by_client_id(self, g->id); - if (img == NULL) { set_command_failed_response("ENOENT", "Put command refers to non-existent image with id: %u", g->id); return; } - 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; } + if (img == NULL) { + if (g->id) img = img_by_client_id(self, g->id); + else if (g->image_number) img = img_by_client_number(self, g->image_number); + 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; } + } + 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; } ensure_space_for(img, refs, ImageRef, img->refcnt + 1, refcap, 16, true); *is_dirty = true; self->layers_dirty = true; @@ -556,6 +606,7 @@ handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, b update_dest_rect(ref, g->num_cells, g->num_lines, cell); // Move the cursor, the screen will take care of ensuring it is in bounds c->x += ref->effective_num_cols; c->y += ref->effective_num_rows - 1; + return img->client_id; } static int @@ -649,7 +700,8 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree // Image lifetime/scrolling {{{ static inline void -filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(const ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) { +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) { + bool matched = false; for (size_t i = self->image_count; i-- > 0;) { Image *img = self->images + i; for (size_t j = img->refcnt; j-- > 0;) { @@ -657,12 +709,15 @@ filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*fi if (filter_func(ref, img, data, cell)) { remove_i_from_array(img->refs, j, img->refcnt); self->layers_dirty = true; + matched = true; } } if (img->refcnt == 0 && (free_images || img->client_id == 0)) remove_image(self, i); + if (only_first_image && matched) break; } } + static inline void modify_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(ImageRef*, Image*, const void*, CellPixelSize), CellPixelSize cell) { 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 void grman_clear(GraphicsManager *self, bool all, CellPixelSize cell) { - filter_refs(self, NULL, true, all ? clear_all_filter_func : clear_filter_func, cell); + filter_refs(self, NULL, true, all ? clear_all_filter_func : clear_filter_func, cell, false); } static inline bool @@ -753,6 +808,14 @@ id_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize return false; } +static inline bool +number_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell UNUSED) { + const GraphicsCommand *g = data; + if (img->client_number == g->image_number) return !g->placement_id || ref->client_id == g->placement_id; + return false; +} + + static inline bool x_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) { const GraphicsCommand *g = data; @@ -786,8 +849,9 @@ point3d_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixel static void handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, CellPixelSize cell) { static GraphicsCommand d; + bool only_first_image = false; switch (g->delete_action) { -#define I(u, data, func) filter_refs(self, data, g->delete_action == u, func, cell); *is_dirty = true; break +#define I(u, data, func) filter_refs(self, data, g->delete_action == u, func, cell, only_first_image); *is_dirty = true; break #define D(l, u, data, func) case l: case u: I(u, data, func) #define G(l, u, func) D(l, u, g, func) case 0: @@ -802,6 +866,10 @@ handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c case 'C': d.x_offset = c->x + 1; d.y_offset = c->y + 1; I('C', &d, point_filter_func); + case 'n': + case 'N': + only_first_image = true; + I('N', &g, number_filter_func); default: REPORT_ERROR("Unknown graphics command delete action: %c", g->delete_action); break; @@ -839,6 +907,11 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint const char *ret = NULL; command_response[0] = 0; + if (g->id && g->image_number) { + set_command_failed_response("EINVAL", "Must not specify both image id and image number"); + return finish_command_response(g, false, g->id, g->placement_id, g->image_number); + } + switch(g->action) { case 0: case 't': @@ -848,8 +921,8 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint bool is_query = g->action == 'q'; if (is_query) { iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } } Image *image = handle_add_command(self, g, payload, is_dirty, iid); - if (is_query) ret = finish_command_response(g, image != NULL, q_iid, 0); - else ret = finish_command_response(g, image != NULL, self->last_init_graphics_command.id, self->last_init_graphics_command.placement_id); + if (is_query) ret = finish_command_response(g, image != NULL, q_iid, 0, 0); + 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); 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); id_type added_image_id = image ? image->internal_id : 0; if (g->action == 'q') remove_images(self, add_trim_predicate, 0); @@ -857,12 +930,12 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint break; } case 'p': - if (!g->id) { - REPORT_ERROR("Put graphics command without image id"); + if (!g->id && !g->image_number) { + REPORT_ERROR("Put graphics command without image id or number"); break; } - handle_put_command(self, g, c, is_dirty, NULL, cell); - ret = finish_command_response(g, true, g->id, g->placement_id); + uint32_t image_id = handle_put_command(self, g, c, is_dirty, NULL, cell); + ret = finish_command_response(g, true, image_id, g->placement_id, g->image_number); break; case 'd': handle_delete_command(self, g, c, is_dirty, cell); @@ -886,8 +959,8 @@ new(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) { static inline PyObject* image_as_dict(Image *img) { #define U(x) #x, img->x - return Py_BuildValue("{sI sI sI sI sK sI sO sO sN}", - U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), + return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sN}", + U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number), "data_loaded", img->data_loaded ? Py_True : Py_False, "is_4byte_aligned", img->load_data.is_4byte_aligned ? Py_True : Py_False, "data", Py_BuildValue("y#", img->load_data.data, img->load_data.data_sz) @@ -907,6 +980,13 @@ W(image_for_client_id) { return image_as_dict(img); } +W(image_for_client_number) { + unsigned long num = PyLong_AsUnsignedLong(args); + Image *img = img_by_client_number(self, num); + if (!img) Py_RETURN_NONE; + return image_as_dict(img); +} + W(shm_write) { const char *name, *data; Py_ssize_t sz; @@ -957,6 +1037,7 @@ W(update_layers) { static PyMethodDef methods[] = { M(image_for_client_id, METH_O), + M(image_for_client_number, METH_O), M(update_layers, METH_VARARGS), {NULL} /* Sentinel */ }; diff --git a/kitty/graphics.h b/kitty/graphics.h index 16efa5f30..af60edaa2 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -10,7 +10,7 @@ typedef struct { unsigned char action, transmission_type, compressed, delete_action; - uint32_t format, more, id, data_sz, data_offset, placement_id, quiet; + uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet; uint32_t width, height, x_offset, y_offset, data_height, data_width, num_cells, num_lines, cell_x_offset, cell_y_offset; int32_t z_index; size_t payload_sz; @@ -44,7 +44,7 @@ typedef struct { typedef struct { - uint32_t texture_id, client_id, width, height; + uint32_t texture_id, client_id, client_number, width, height; id_type internal_id; bool data_loaded; diff --git a/kitty/parse-graphics-command.h b/kitty/parse-graphics-command.h index 5418b157e..5c5f946dd 100644 --- a/kitty/parse-graphics-command.h +++ b/kitty/parse-graphics-command.h @@ -24,6 +24,7 @@ static inline void parse_graphics_code(Screen *screen, format = 'f', more = 'm', id = 'i', + image_number = 'I', placement_id = 'p', quiet = 'q', width = 'w', @@ -72,6 +73,9 @@ static inline void parse_graphics_code(Screen *screen, case id: value_state = UINT; break; + case image_number: + value_state = UINT; + break; case placement_id: value_state = UINT; break; @@ -140,8 +144,8 @@ static inline void parse_graphics_code(Screen *screen, case action: { g.action = screen->parser_buf[pos++] & 0xff; - if (g.action != 'q' && g.action != 'd' && g.action != 'p' && - g.action != 'T' && g.action != 't') { + if (g.action != 'T' && g.action != 'd' && g.action != 'p' && + g.action != 't' && g.action != 'q') { REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " "value for action: 0x%x", g.action); @@ -151,14 +155,14 @@ static inline void parse_graphics_code(Screen *screen, case delete_action: { g.delete_action = screen->parser_buf[pos++] & 0xff; - if (g.delete_action != 'y' && g.delete_action != 'q' && - g.delete_action != 'z' && g.delete_action != 'Z' && - g.delete_action != 'a' && g.delete_action != 'A' && - g.delete_action != 'Q' && g.delete_action != 'X' && - g.delete_action != 'i' && g.delete_action != 'p' && - g.delete_action != 'Y' && g.delete_action != 'P' && - g.delete_action != 'c' && g.delete_action != 'x' && - g.delete_action != 'C' && g.delete_action != 'I') { + if (g.delete_action != 'y' && g.delete_action != 'c' && + g.delete_action != 'x' && g.delete_action != 'p' && + g.delete_action != 'P' && g.delete_action != 'Q' && + g.delete_action != 'C' && g.delete_action != 'Y' && + g.delete_action != 'i' && g.delete_action != 'z' && + g.delete_action != 'A' && g.delete_action != 'a' && + g.delete_action != 'q' && g.delete_action != 'Z' && + g.delete_action != 'X' && g.delete_action != 'I') { REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " "value for delete_action: 0x%x", g.delete_action); @@ -168,8 +172,8 @@ static inline void parse_graphics_code(Screen *screen, case transmission_type: { g.transmission_type = screen->parser_buf[pos++] & 0xff; - if (g.transmission_type != 'f' && g.transmission_type != 's' && - g.transmission_type != 't' && g.transmission_type != 'd') { + if (g.transmission_type != 's' && g.transmission_type != 'd' && + g.transmission_type != 't' && g.transmission_type != 'f') { REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " "value for transmission_type: 0x%x", g.transmission_type); @@ -242,6 +246,7 @@ static inline void parse_graphics_code(Screen *screen, U(format); U(more); U(id); + U(image_number); U(placement_id); U(quiet); U(width); @@ -314,12 +319,13 @@ static inline void parse_graphics_code(Screen *screen, } REPORT_VA_COMMAND( - "s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI si " + "s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI si " "sI} y#", "graphics_command", "action", g.action, "delete_action", g.delete_action, "transmission_type", g.transmission_type, "compressed", g.compressed, "format", (unsigned int)g.format, "more", (unsigned int)g.more, "id", - (unsigned int)g.id, "placement_id", (unsigned int)g.placement_id, "quiet", + (unsigned int)g.id, "image_number", (unsigned int)g.image_number, + "placement_id", (unsigned int)g.placement_id, "quiet", (unsigned int)g.quiet, "width", (unsigned int)g.width, "height", (unsigned int)g.height, "x_offset", (unsigned int)g.x_offset, "y_offset", (unsigned int)g.y_offset, "data_height", (unsigned int)g.data_height, diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index 0801369e4..1342976e8 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -231,6 +231,54 @@ class TestGraphics(BaseTest): # test error handling for loading bad png data self.assertRaisesRegex(ValueError, '[EBADPNG]', load_png_data, b'dsfsdfsfsfd') + def test_load_with_numbers(self): + s = self.create_screen() + g = s.grman + + def li(payload, **kw): + cmd = ','.join('%s=%s' % (k, v) for k, v in kw.items()) + res = send_command(s, cmd, payload) + return parse_response_with_ids(res) + + code, ids = li('abc', s=1, v=1, f=24, I=1, i=3) + self.ae(code, 'EINVAL') + + code, ids = li('abc', s=1, v=1, f=24, I=1) + self.ae((code, ids), ('OK', 'i=1,I=1')) + img = g.image_for_client_number(1) + self.ae(img['client_number'], 1) + self.ae(img['client_id'], 1) + code, ids = li('abc', s=1, v=1, f=24, I=1) + self.ae((code, ids), ('OK', 'i=2,I=1')) + img = g.image_for_client_number(1) + self.ae(img['client_number'], 1) + self.ae(img['client_id'], 2) + code, ids = li('abc', s=1, v=1, f=24, I=1) + self.ae((code, ids), ('OK', 'i=3,I=1')) + code, ids = li('abc', s=1, v=1, f=24, i=5) + self.ae((code, ids), ('OK', 'i=5')) + code, ids = li('abc', s=1, v=1, f=24, I=3) + self.ae((code, ids), ('OK', 'i=4,I=3')) + + # Test chunked load with number + self.assertIsNone(li('abcd', s=2, v=2, m=1, I=93)) + self.assertIsNone(li('efgh', m=1)) + self.assertIsNone(li('ijkx', m=1)) + self.ae(li('mnop', m=0), ('OK', 'i=6,I=93')) + img = g.image_for_client_number(93) + self.ae(img['data'], b'abcdefghijkxmnop') + self.ae(img['client_id'], 6) + + # test put with number + def put(**kw): + cmd = ','.join('%s=%s' % (k, v) for k, v in kw.items()) + cmd = 'a=p,' + cmd + return parse_response_with_ids(send_command(s, cmd)) + + code, idstr = put(c=2, r=2, I=93) + self.ae((code, idstr), ('OK', 'i=6,I=93')) + self.assertIsNone(put(c=2, r=2, I=94)) + def test_image_put(self): cw, ch = 10, 20 s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch) diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index cd73f574d..cf1de0b59 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -379,7 +379,7 @@ class TestParser(BaseTest): for f in 'action delete_action transmission_type compressed'.split(): k.setdefault(f, b'\0') for f in ('format more id data_sz data_offset width height x_offset y_offset data_height data_width' - ' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id quiet').split(): + ' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id image_number quiet').split(): k.setdefault(f, 0) p = k.pop('payload', '').encode('utf-8') k['payload_sz'] = len(p)