diff --git a/docs/changelog.rst b/docs/changelog.rst index 6314bc22b..4f62baf45 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,9 @@ To update |kitty|, :doc:`follow the instructions `. - Add a new mappable `select_tab` action to choose a tab to switch to even when the tab bar is hidden (:iss:`3115`) +- Graphics protocol: Add support for giving individual image placements their + own ids. This is a backwards compatible protocol extension. (:iss:`3133`) + - 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 96f9b9632..67b14dec1 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -344,6 +344,25 @@ scheme described above for querying available transmission media, except that here we are querying if the image with the specified id is available or needs to be re-transmitted. +Since there can be many placements per image, you can also give placements an +id. To do so add the ``p`` key with a number between ``1`` and ``4294967295``. +When you specify a placement id, it will be added to the acknowledgement code +above. Every placement is uniquely identified by the pair of the ``image id`` +and the ``placement id``. If you specify a placement id for an image that does +not have an id, it will be ignored. An example response:: + + _Gi=,p=;OK\ + +If you send two placements with the same ``image id`` and ``placement id`` the +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. + + Controlling displayed image layout ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -386,24 +405,30 @@ scrollback buffer. The values of the ``x`` and ``y`` keys are the same as cursor ================= ============ Value of ``d`` Meaning ================= ============ -``a`` or ``A`` Delete all images visible on screen -``i`` or ``I`` Delete all images with the specified id, specified using the ``i`` key. -``c`` or ``C`` Delete all images that intersect with the current cursor position. -``p`` or ``P`` Delete all images that intersect a specific cell, the cell is specified using the ``x`` and ``y`` keys -``q`` or ``Q`` Delete all images that intersect a specific cell having a specific z-index. The cell and z-index is specified using the ``x``, ``y`` and ``z`` keys. -``x`` or ``X`` Delete all images that intersect the specified column, specified using the ``x`` key. -``y`` or ``Y`` Delete all images that intersect the specified row, specified using the ``y`` key. -``z`` or ``Z`` Delete all images that have the specified z-index, specified using the ``z`` key. +``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 +``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. +``x`` or ``X`` Delete all placements that intersect the specified column, specified using the ``x`` key. +``y`` or ``Y`` Delete all placements that intersect the specified row, specified using the ``y`` key. +``z`` or ``Z`` Delete all placements that have the specified z-index, specified using the ``z`` key. ================= ============ +Note when all placements for an image have been deleted, the image is also +deleted, if the capital letter form above is specified. Also, when the terminal +is running out of quota space for image, images without placements will be +preferentially deleted. Some examples:: - _Ga=d\ # delete all visible images - _Ga=d,d=i,i=10\ # delete the image with id=10, without freeing data - _Ga=d,d=Z,z=-1\ # delete the images with z-index -1, also freeing up image data - _Ga=d,d=p,x=3,y=4\ # delete all images that intersect the cell at (3, 4), without freeing data + _Ga=d\ # delete all visible placements + _Ga=d,d=i,i=10\ # delete the image with id=10, without freeing data + _Ga=d,d=i,i=10,p=7\ # delete the image with id=10 and placement id=7, without freeing data + _Ga=d,d=Z,z=-1\ # delete the placements with z-index -1, also freeing up image data + _Ga=d,d=p,x=3,y=4\ # delete all placements that intersect the cell at (3, 4), without freeing data Image persistence and storage quotas ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -438,6 +463,8 @@ Key Value Default Description ``O`` Positive integer. ``0`` The offset from which to read data from a file. ``i`` Positive integer. ``(0 - 4294967295)`` ``0`` The image id +``p`` Positive integer. + ``(0 - 4294967295)`` ``0`` The placement id ``o`` Single character. ``null`` The type of data compression. ``only z`` ``m`` zero or one ``0`` Whether there is more chunked data available. diff --git a/gen-apc-parsers.py b/gen-apc-parsers.py index fbc298b58..0cc3db9d0 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'), + 'p': ('placement_id', 'uint'), 'w': ('width', 'uint'), 'h': ('height', 'uint'), 'x': ('x_offset', 'uint'), diff --git a/kitty/graphics.c b/kitty/graphics.c index 07e55c080..4cfaf00bf 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -475,14 +475,15 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ } static inline const char* -create_add_response(GraphicsManager UNUSED *self, bool data_loaded, uint32_t iid) { - static char rbuf[sizeof(add_response)/sizeof(add_response[0]) + 64]; +create_add_response(GraphicsManager UNUSED *self, bool data_loaded, uint32_t iid, uint32_t placement_id) { + static char rbuf[sizeof(add_response)/sizeof(add_response[0]) + 128]; if (iid) { if (!has_add_respose) { if (!data_loaded) return NULL; snprintf(add_response, 10, "OK"); } - snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u;%s", iid, add_response); + if (placement_id) snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u,p=%u;%s", iid, placement_id, add_response); + else snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u;%s", iid, add_response); return rbuf; } return NULL; @@ -529,10 +530,12 @@ handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, b *is_dirty = true; self->layers_dirty = true; ImageRef *ref = NULL; - for (size_t i=0; i < img->refcnt; i++) { - if ((unsigned)img->refs[i].start_row == c->x && (unsigned)img->refs[i].start_column == c->y) { - ref = img->refs + i; - break; + if (g->placement_id && img->client_id) { + for (size_t i=0; i < img->refcnt; i++) { + if (img->refs[i].client_id == g->placement_id) { + ref = img->refs + i; + break; + } } } if (ref == NULL) { @@ -548,6 +551,7 @@ handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, b ref->cell_x_offset = MIN(g->cell_x_offset, cell.width - 1); ref->cell_y_offset = MIN(g->cell_y_offset, cell.height - 1); ref->num_cols = g->num_cells; ref->num_rows = g->num_lines; + if (img->client_id) ref->client_id = g->placement_id; update_src_rect(ref, img); 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 @@ -743,9 +747,10 @@ grman_clear(GraphicsManager *self, bool all, CellPixelSize cell) { } static inline bool -id_filter_func(const ImageRef UNUSED *ref, Image *img, const void *data, CellPixelSize cell UNUSED) { - uint32_t iid = *(uint32_t*)data; - return img->client_id == iid; +id_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell UNUSED) { + const GraphicsCommand *g = data; + if (img->client_id == g->id) return !g->placement_id || ref->client_id == g->placement_id; + return false; } static inline bool @@ -787,7 +792,7 @@ handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c #define G(l, u, func) D(l, u, g, func) case 0: D('a', 'A', NULL, clear_filter_func); - D('i', 'I', &g->id, id_filter_func); + G('i', 'I', id_filter_func); G('p', 'P', point_filter_func); G('q', 'Q', point3d_filter_func); G('x', 'X', x_filter_func); @@ -839,9 +844,11 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint case 'T': case 'q': { uint32_t iid = g->id, q_iid = iid; - if (g->action == 'q') { iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } } + 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); - ret = create_add_response(self, image != NULL, g->action == 'q' ? q_iid: self->last_init_graphics_command.id); + if (is_query) ret = create_add_response(self, image != NULL, q_iid, 0); + else ret = create_add_response(self, image != NULL, self->last_init_graphics_command.id, self->last_init_graphics_command.placement_id); 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); @@ -854,7 +861,7 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint break; } handle_put_command(self, g, c, is_dirty, NULL, cell); - ret = create_add_response(self, true, g->id); + ret = create_add_response(self, true, g->id, g->placement_id); break; case 'd': handle_delete_command(self, g, c, is_dirty, cell); diff --git a/kitty/graphics.h b/kitty/graphics.h index 4a951b385..ff0f8600a 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; + uint32_t format, more, id, data_sz, data_offset, placement_id; 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; @@ -38,6 +38,7 @@ typedef struct { uint32_t cell_x_offset, cell_y_offset, num_cols, num_rows, effective_num_rows, effective_num_cols; int32_t z_index; int32_t start_row, start_column; + uint32_t client_id; ImageRect src_rect; } ImageRef; diff --git a/kitty/parse-graphics-command.h b/kitty/parse-graphics-command.h index b77865120..ed2f49f0b 100644 --- a/kitty/parse-graphics-command.h +++ b/kitty/parse-graphics-command.h @@ -23,6 +23,7 @@ static inline void parse_graphics_code(Screen *screen, format = 'f', more = 'm', id = 'i', + placement_id = 'p', width = 'w', height = 'h', x_offset = 'x', @@ -69,6 +70,9 @@ static inline void parse_graphics_code(Screen *screen, case id: value_state = UINT; break; + case placement_id: + value_state = UINT; + break; case width: value_state = UINT; break; @@ -131,8 +135,8 @@ static inline void parse_graphics_code(Screen *screen, case action: { g.action = screen->parser_buf[pos++] & 0xff; - if (g.action != 't' && g.action != 'd' && g.action != 'p' && - g.action != 'q' && g.action != 'T') { + if (g.action != 'p' && g.action != 'q' && g.action != 't' && + g.action != 'T' && g.action != 'd') { REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " "value for action: 0x%x", g.action); @@ -142,14 +146,14 @@ static inline void parse_graphics_code(Screen *screen, case delete_action: { g.delete_action = screen->parser_buf[pos++] & 0xff; - if (g.delete_action != 'X' && g.delete_action != 'y' && + if (g.delete_action != 'p' && g.delete_action != 'q' && + g.delete_action != 'Z' && g.delete_action != 'x' && + g.delete_action != 'Q' && g.delete_action != 'C' && + g.delete_action != 'y' && g.delete_action != 'Y' && g.delete_action != 'i' && g.delete_action != 'I' && - g.delete_action != 'A' && g.delete_action != 'p' && - g.delete_action != 'Y' && g.delete_action != 'z' && - g.delete_action != 'a' && g.delete_action != 'P' && - g.delete_action != 'x' && g.delete_action != 'q' && - g.delete_action != 'Z' && g.delete_action != 'Q' && - g.delete_action != 'c' && g.delete_action != 'C') { + g.delete_action != 'a' && g.delete_action != 'z' && + g.delete_action != 'A' && g.delete_action != 'X' && + g.delete_action != 'P' && g.delete_action != 'c') { REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " "value for delete_action: 0x%x", g.delete_action); @@ -159,8 +163,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 != 'd' && - g.transmission_type != 's' && g.transmission_type != 't') { + if (g.transmission_type != 'd' && g.transmission_type != 'f' && + g.transmission_type != 't' && g.transmission_type != 's') { REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " "value for transmission_type: 0x%x", g.transmission_type); @@ -233,6 +237,7 @@ static inline void parse_graphics_code(Screen *screen, U(format); U(more); U(id); + U(placement_id); U(width); U(height); U(x_offset); @@ -303,19 +308,21 @@ 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} y#", + "s {sc sc sc sc 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, "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, - "data_width", (unsigned int)g.data_width, "data_sz", - (unsigned int)g.data_sz, "data_offset", (unsigned int)g.data_offset, - "num_cells", (unsigned int)g.num_cells, "num_lines", - (unsigned int)g.num_lines, "cell_x_offset", (unsigned int)g.cell_x_offset, - "cell_y_offset", (unsigned int)g.cell_y_offset, "z_index", (int)g.z_index, - "payload_sz", g.payload_sz, payload, g.payload_sz); + (unsigned int)g.id, "placement_id", (unsigned int)g.placement_id, "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, "data_width", + (unsigned int)g.data_width, "data_sz", (unsigned int)g.data_sz, + "data_offset", (unsigned int)g.data_offset, "num_cells", + (unsigned int)g.num_cells, "num_lines", (unsigned int)g.num_lines, + "cell_x_offset", (unsigned int)g.cell_x_offset, "cell_y_offset", + (unsigned int)g.cell_y_offset, "z_index", (int)g.z_index, "payload_sz", + g.payload_sz, payload, g.payload_sz); screen_handle_graphics_command(screen, &g, payload); } diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index 89853cda4..a91025e21 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -48,6 +48,15 @@ def parse_response(res): return res.decode('ascii').partition(';')[2].partition('\033')[0] +def parse_response_with_ids(res): + if not res: + return + a, b = res.decode('ascii').split(';', 1) + code = b.partition('\033')[0].split(':', 1)[0] + a = a.split('G', 1)[1] + return code, a + + all_bytes = bytes(bytearray(range(256))) @@ -90,8 +99,9 @@ def put_helpers(self, cw, ch): s = self.create_screen(10, 5, cell_width=cw, cell_height=ch) return s, 2 / s.columns, 2 / s.lines - def put_cmd(z=0, num_cols=0, num_lines=0, x_off=0, y_off=0, width=0, height=0, cell_x_off=0, cell_y_off=0): - return 'z=%d,c=%d,r=%d,x=%d,y=%d,w=%d,h=%d,X=%d,Y=%d' % (z, num_cols, num_lines, x_off, y_off, width, height, cell_x_off, cell_y_off) + def put_cmd(z=0, num_cols=0, num_lines=0, x_off=0, y_off=0, width=0, height=0, cell_x_off=0, cell_y_off=0, placement_id=0): + return 'z=%d,c=%d,r=%d,x=%d,y=%d,w=%d,h=%d,X=%d,Y=%d,p=%d' % ( + z, num_cols, num_lines, x_off, y_off, width, height, cell_x_off, cell_y_off, placement_id) def put_image(screen, w, h, **kw): nonlocal iid @@ -103,7 +113,7 @@ def put_helpers(self, cw, ch): def put_ref(screen, **kw): cmd = 'a=p,i=%d,%s' % (iid, put_cmd(**kw)) - send_command(screen, cmd) + return iid, parse_response_with_ids(send_command(screen, cmd)) def layers(screen, scrolled_by=0, xstart=-1, ystart=1): return screen.grman.update_layers(scrolled_by, xstart, ystart, dx, dy, screen.columns, screen.lines, cw, ch) @@ -226,7 +236,8 @@ class TestGraphics(BaseTest): rect_eq(l0[0]['dest_rect'], -1, 1, -1 + dx, 1 - dy) self.ae(l0[0]['group_count'], 1) self.ae(s.cursor.x, 1), self.ae(s.cursor.y, 0) - put_ref(s, num_cols=s.columns, x_off=2, y_off=1, width=3, height=5, cell_x_off=3, cell_y_off=1, z=-1) + iid, (code, idstr) = put_ref(s, num_cols=s.columns, x_off=2, y_off=1, width=3, height=5, cell_x_off=3, cell_y_off=1, z=-1, placement_id=17) + self.ae(idstr, f'i={iid},p=17') l2 = layers(s) self.ae(len(l2), 2) rect_eq(l2[0]['src_rect'], 2 / 10, 1 / 20, (2 + 3) / 10, (1 + 5)/20) @@ -320,8 +331,13 @@ class TestGraphics(BaseTest): delete('A') self.ae(s.grman.image_count, 0) iid = put_image(s, cw, ch)[0] + delete('I', i=iid, p=7) + self.ae(s.grman.image_count, 1) delete('I', i=iid) self.ae(s.grman.image_count, 0) + iid = put_image(s, cw, ch, placement_id=9)[0] + delete('I', i=iid, p=9) + self.ae(s.grman.image_count, 0) s.reset() put_image(s, cw, ch) put_image(s, cw, ch) diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index 6de47904c..34178a2a4 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').split(): + ' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id').split(): k.setdefault(f, 0) p = k.pop('payload', '').encode('utf-8') k['payload_sz'] = len(p) @@ -395,6 +395,7 @@ class TestParser(BaseTest): pb = partial(self.parse_bytes_dump, s) uint32_max = 2**32 - 1 t('i=%d' % uint32_max, id=uint32_max) + t('i=3,p=4', id=3, placement_id=4) e('i=%d' % (uint32_max + 1), 'Malformed GraphicsCommand control block, number is too large') pb('\033_Gi=12\033\\', c(id=12)) t('a=t,t=d,s=100,z=-9', payload='X', action='t', transmission_type='d', data_width=100, z_index=-9, payload_sz=1)