Graphics protocol: Add support for giving individual image placements their

Fixes #3133
This commit is contained in:
Kovid Goyal 2020-12-02 05:23:08 +05:30
parent 0173959e64
commit b5e704a934
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 116 additions and 53 deletions

View File

@ -13,6 +13,9 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- 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`)

View File

@ -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::
<ESC>_Gi=<image id>,p=<placement id>;OK<ESC>\
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::
<ESC>_Ga=d<ESC>\ # delete all visible images
<ESC>_Ga=d,d=i,i=10<ESC>\ # delete the image with id=10, without freeing data
<ESC>_Ga=d,d=Z,z=-1<ESC>\ # delete the images with z-index -1, also freeing up image data
<ESC>_Ga=d,d=p,x=3,y=4<ESC>\ # delete all images that intersect the cell at (3, 4), without freeing data
<ESC>_Ga=d<ESC>\ # delete all visible placements
<ESC>_Ga=d,d=i,i=10<ESC>\ # delete the image with id=10, without freeing data
<ESC>_Ga=d,d=i,i=10,p=7<ESC>\ # delete the image with id=10 and placement id=7, without freeing data
<ESC>_Ga=d,d=Z,z=-1<ESC>\ # delete the placements with z-index -1, also freeing up image data
<ESC>_Ga=d,d=p,x=3,y=4<ESC>\ # 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.

View File

@ -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'),

View File

@ -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);

View File

@ -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;

View File

@ -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);
}

View File

@ -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 create_screen():
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_image(screen, w, h, **kw):
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 @@ def test_image_put(self):
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 @@ def delete(ac=None, **kw):
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)

View File

@ -379,7 +379,7 @@ def c(**k):
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 @@ def e(cmd, err):
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)