Graphics protocol: Allow suppressing responses from the terminal to graphics commands

This commit is contained in:
Kovid Goyal 2020-12-03 20:42:03 +05:30
parent c9828dfece
commit 23420adfa6
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 79 additions and 52 deletions

View File

@ -14,7 +14,8 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
when the tab bar is hidden (:iss:`3115`) when the tab bar is hidden (:iss:`3115`)
- Graphics protocol: Add support for giving individual image placements their - Graphics protocol: Add support for giving individual image placements their
own ids. This is a backwards compatible protocol extension. (:iss:`3133`) own ids. This is a backwards compatible protocol extension, and also allow
suppressing responses from the terminal to commands (:iss:`3133`)
- Distribute extra pixels among all eight-blocks rather than adding them - Distribute extra pixels among all eight-blocks rather than adding them
all to the last block (:iss:`3097`) all to the last block (:iss:`3097`)

View File

@ -430,6 +430,18 @@ Some examples::
<ESC>_Ga=d,d=Z,z=-1<ESC>\ # delete the placements with z-index -1, also freeing up image 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 <ESC>_Ga=d,d=p,x=3,y=4<ESC>\ # delete all placements that intersect the cell at (3, 4), without freeing data
Suppressing responses from the terminal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are using the graphics protocol from a limited client, such as a shell
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
Image persistence and storage quotas Image persistence and storage quotas
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -451,6 +463,9 @@ Key Value Default Description
======= ==================== ========= ================= ======= ==================== ========= =================
``a`` Single character. ``t`` The overall action this graphics command is performing. ``a`` Single character. ``t`` The overall action this graphics command is performing.
``(t, T, q, p, d)`` ``(t, T, q, p, d)``
``q`` ``0, 1, 2`` ``0`` Suppress responses from the terminal to this graphics command.
**Keys for image transmission** **Keys for image transmission**
----------------------------------------------------------- -----------------------------------------------------------
``f`` Positive integer. ``32`` The format in which the image data is sent. ``f`` Positive integer. ``32`` The format in which the image data is sent.

View File

@ -258,6 +258,7 @@ def graphics_parser() -> None:
'm': ('more', 'uint'), 'm': ('more', 'uint'),
'i': ('id', 'uint'), 'i': ('id', 'uint'),
'p': ('placement_id', 'uint'), 'p': ('placement_id', 'uint'),
'q': ('quiet', 'uint'),
'w': ('width', 'uint'), 'w': ('width', 'uint'),
'h': ('height', 'uint'), 'h': ('height', 'uint'),
'x': ('x_offset', 'uint'), 'x': ('x_offset', 'uint'),

View File

@ -137,22 +137,20 @@ apply_storage_quota(GraphicsManager *self, size_t storage_limit, id_type current
if (!self->image_count) self->used_storage = 0; // sanity check if (!self->image_count) self->used_storage = 0; // sanity check
} }
static char add_response[512] = {0}; static char command_response[512] = {0};
static bool has_add_respose = false;
static inline void static inline void
set_add_response(const char *code, const char *fmt, ...) { set_command_failed_response(const char *code, const char *fmt, ...) {
va_list args; va_list args;
va_start(args, fmt); va_start(args, fmt);
size_t sz = sizeof(add_response)/sizeof(add_response[0]); const size_t sz = sizeof(command_response)/sizeof(command_response[0]);
int num = snprintf(add_response, sz, "%s:", code); const int num = snprintf(command_response, sz, "%s:", code);
vsnprintf(add_response + num, sz - num, fmt, args); vsnprintf(command_response + num, sz - num, fmt, args);
va_end(args); va_end(args);
has_add_respose = true;
} }
// Decode formats {{{ // Decode formats {{{
#define ABRT(code, ...) { set_add_response(#code, __VA_ARGS__); goto err; } #define ABRT(code, ...) { set_command_failed_response(#code, __VA_ARGS__); goto err; }
static inline bool static inline bool
mmap_img_file(GraphicsManager UNUSED *self, Image *img, int fd, size_t sz, off_t offset) { mmap_img_file(GraphicsManager UNUSED *self, Image *img, int fd, size_t sz, off_t offset) {
@ -220,7 +218,7 @@ err:
static void static void
png_error_handler(const char *code, const char *msg) { png_error_handler(const char *code, const char *msg) {
set_add_response(code, "%s", msg); set_command_failed_response(code, "%s", msg);
} }
static inline bool static inline bool
@ -311,9 +309,8 @@ find_or_create_image(GraphicsManager *self, uint32_t id, bool *existing) {
static Image* static Image*
handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, bool *is_dirty, uint32_t iid) { handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, bool *is_dirty, uint32_t iid) {
#define ABRT(code, ...) { set_add_response(#code, __VA_ARGS__); self->loading_image = 0; if (img) img->data_loaded = false; return NULL; } #define ABRT(code, ...) { set_command_failed_response(#code, __VA_ARGS__); self->loading_image = 0; if (img) img->data_loaded = false; return NULL; }
#define MAX_DATA_SZ (4u * 100000000u) #define MAX_DATA_SZ (4u * 100000000u)
has_add_respose = false;
bool existing, init_img = true; bool existing, init_img = true;
Image *img = NULL; Image *img = NULL;
unsigned char tt = g->transmission_type ? g->transmission_type : 'd'; unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
@ -475,15 +472,19 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
} }
static inline const char* static inline const char*
create_add_response(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) {
static char rbuf[sizeof(add_response)/sizeof(add_response[0]) + 128]; static char rbuf[sizeof(command_response)/sizeof(command_response[0]) + 128];
if (iid) { bool is_ok_response = !command_response[0];
if (!has_add_respose) { if (g->quiet) {
if (!data_loaded) return NULL; if (is_ok_response || g->quiet > 1) return NULL;
snprintf(add_response, 10, "OK");
} }
if (placement_id) snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u,p=%u;%s", iid, placement_id, add_response); if (iid) {
else snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "Gi=%u;%s", iid, add_response); if (is_ok_response) {
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);
return rbuf; return rbuf;
} }
return NULL; return NULL;
@ -522,10 +523,9 @@ update_dest_rect(ImageRef *ref, uint32_t num_cols, uint32_t num_rows, CellPixelS
static void static void
handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img, CellPixelSize cell) { handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img, CellPixelSize cell) {
has_add_respose = false;
if (img == NULL) img = img_by_client_id(self, g->id); if (img == NULL) img = img_by_client_id(self, g->id);
if (img == NULL) { set_add_response("ENOENT", "Put command refers to non-existent image with id: %u", g->id); return; } 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_add_response("ENOENT", "Put command refers to image with id: %u that could not load its data", 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; }
ensure_space_for(img, refs, ImageRef, img->refcnt + 1, refcap, 16, true); ensure_space_for(img, refs, ImageRef, img->refcnt + 1, refcap, 16, true);
*is_dirty = true; *is_dirty = true;
self->layers_dirty = true; self->layers_dirty = true;
@ -837,6 +837,7 @@ grman_rescale(GraphicsManager *self, CellPixelSize cell) {
const char* const char*
grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty, CellPixelSize cell) { grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty, CellPixelSize cell) {
const char *ret = NULL; const char *ret = NULL;
command_response[0] = 0;
switch(g->action) { switch(g->action) {
case 0: case 0:
@ -847,8 +848,8 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint
bool is_query = g->action == 'q'; bool is_query = g->action == 'q';
if (is_query) { iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } } 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); Image *image = handle_add_command(self, g, payload, is_dirty, iid);
if (is_query) ret = create_add_response(image != NULL, q_iid, 0); if (is_query) ret = finish_command_response(g, image != NULL, q_iid, 0);
else ret = create_add_response(image != NULL, self->last_init_graphics_command.id, self->last_init_graphics_command.placement_id); else ret = finish_command_response(g, 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); 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; id_type added_image_id = image ? image->internal_id : 0;
if (g->action == 'q') remove_images(self, add_trim_predicate, 0); if (g->action == 'q') remove_images(self, add_trim_predicate, 0);
@ -861,7 +862,7 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint
break; break;
} }
handle_put_command(self, g, c, is_dirty, NULL, cell); handle_put_command(self, g, c, is_dirty, NULL, cell);
ret = create_add_response(true, g->id, g->placement_id); ret = finish_command_response(g, true, g->id, g->placement_id);
break; break;
case 'd': case 'd':
handle_delete_command(self, g, c, is_dirty, cell); handle_delete_command(self, g, c, is_dirty, cell);

View File

@ -10,7 +10,7 @@
typedef struct { typedef struct {
unsigned char action, transmission_type, compressed, delete_action; unsigned char action, transmission_type, compressed, delete_action;
uint32_t format, more, id, data_sz, data_offset, placement_id; uint32_t format, more, id, 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; 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; int32_t z_index;
size_t payload_sz; size_t payload_sz;

View File

@ -1,4 +1,5 @@
// This file is generated by ./gen-apc-parsers.py do not edit! // This file is generated by /home/kovid/work/kitty/./gen-apc-parsers.py do not
// edit!
#pragma once #pragma once
@ -24,6 +25,7 @@ static inline void parse_graphics_code(Screen *screen,
more = 'm', more = 'm',
id = 'i', id = 'i',
placement_id = 'p', placement_id = 'p',
quiet = 'q',
width = 'w', width = 'w',
height = 'h', height = 'h',
x_offset = 'x', x_offset = 'x',
@ -73,6 +75,9 @@ static inline void parse_graphics_code(Screen *screen,
case placement_id: case placement_id:
value_state = UINT; value_state = UINT;
break; break;
case quiet:
value_state = UINT;
break;
case width: case width:
value_state = UINT; value_state = UINT;
break; break;
@ -135,8 +140,8 @@ static inline void parse_graphics_code(Screen *screen,
case action: { case action: {
g.action = screen->parser_buf[pos++] & 0xff; g.action = screen->parser_buf[pos++] & 0xff;
if (g.action != 'p' && g.action != 'q' && g.action != 't' && if (g.action != 'q' && g.action != 'd' && g.action != 'p' &&
g.action != 'T' && g.action != 'd') { g.action != 'T' && g.action != 't') {
REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag "
"value for action: 0x%x", "value for action: 0x%x",
g.action); g.action);
@ -146,14 +151,14 @@ static inline void parse_graphics_code(Screen *screen,
case delete_action: { case delete_action: {
g.delete_action = screen->parser_buf[pos++] & 0xff; g.delete_action = screen->parser_buf[pos++] & 0xff;
if (g.delete_action != 'p' && g.delete_action != 'q' && if (g.delete_action != 'y' && g.delete_action != 'q' &&
g.delete_action != 'Z' && g.delete_action != 'x' && g.delete_action != 'z' && g.delete_action != 'Z' &&
g.delete_action != 'Q' && g.delete_action != 'C' && g.delete_action != 'a' && g.delete_action != 'A' &&
g.delete_action != 'y' && g.delete_action != 'Y' && g.delete_action != 'Q' && g.delete_action != 'X' &&
g.delete_action != 'i' && g.delete_action != 'I' && g.delete_action != 'i' && g.delete_action != 'p' &&
g.delete_action != 'a' && g.delete_action != 'z' && g.delete_action != 'Y' && g.delete_action != 'P' &&
g.delete_action != 'A' && g.delete_action != 'X' && g.delete_action != 'c' && g.delete_action != 'x' &&
g.delete_action != 'P' && g.delete_action != 'c') { g.delete_action != 'C' && g.delete_action != 'I') {
REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag "
"value for delete_action: 0x%x", "value for delete_action: 0x%x",
g.delete_action); g.delete_action);
@ -163,8 +168,8 @@ static inline void parse_graphics_code(Screen *screen,
case transmission_type: { case transmission_type: {
g.transmission_type = screen->parser_buf[pos++] & 0xff; g.transmission_type = screen->parser_buf[pos++] & 0xff;
if (g.transmission_type != 'd' && g.transmission_type != 'f' && if (g.transmission_type != 'f' && g.transmission_type != 's' &&
g.transmission_type != 't' && g.transmission_type != 's') { g.transmission_type != 't' && g.transmission_type != 'd') {
REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag " REPORT_ERROR("Malformed GraphicsCommand control block, unknown flag "
"value for transmission_type: 0x%x", "value for transmission_type: 0x%x",
g.transmission_type); g.transmission_type);
@ -238,6 +243,7 @@ static inline void parse_graphics_code(Screen *screen,
U(more); U(more);
U(id); U(id);
U(placement_id); U(placement_id);
U(quiet);
U(width); U(width);
U(height); U(height);
U(x_offset); U(x_offset);
@ -308,21 +314,21 @@ static inline void parse_graphics_code(Screen *screen,
} }
REPORT_VA_COMMAND( 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 "
"y#", "sI} y#",
"graphics_command", "action", g.action, "delete_action", g.delete_action, "graphics_command", "action", g.action, "delete_action", g.delete_action,
"transmission_type", g.transmission_type, "compressed", g.compressed, "transmission_type", g.transmission_type, "compressed", g.compressed,
"format", (unsigned int)g.format, "more", (unsigned int)g.more, "id", "format", (unsigned int)g.format, "more", (unsigned int)g.more, "id",
(unsigned int)g.id, "placement_id", (unsigned int)g.placement_id, "width", (unsigned int)g.id, "placement_id", (unsigned int)g.placement_id, "quiet",
(unsigned int)g.width, "height", (unsigned int)g.height, "x_offset", (unsigned int)g.quiet, "width", (unsigned int)g.width, "height",
(unsigned int)g.x_offset, "y_offset", (unsigned int)g.y_offset, (unsigned int)g.height, "x_offset", (unsigned int)g.x_offset, "y_offset",
"data_height", (unsigned int)g.data_height, "data_width", (unsigned int)g.y_offset, "data_height", (unsigned int)g.data_height,
(unsigned int)g.data_width, "data_sz", (unsigned int)g.data_sz, "data_width", (unsigned int)g.data_width, "data_sz",
"data_offset", (unsigned int)g.data_offset, "num_cells", (unsigned int)g.data_sz, "data_offset", (unsigned int)g.data_offset,
(unsigned int)g.num_cells, "num_lines", (unsigned int)g.num_lines, "num_cells", (unsigned int)g.num_cells, "num_lines",
"cell_x_offset", (unsigned int)g.cell_x_offset, "cell_y_offset", (unsigned int)g.num_lines, "cell_x_offset", (unsigned int)g.cell_x_offset,
(unsigned int)g.cell_y_offset, "z_index", (int)g.z_index, "payload_sz", "cell_y_offset", (unsigned int)g.cell_y_offset, "z_index", (int)g.z_index,
g.payload_sz, payload, g.payload_sz); "payload_sz", g.payload_sz, payload, g.payload_sz);
screen_handle_graphics_command(screen, &g, payload); screen_handle_graphics_command(screen, &g, payload);
} }

View File

@ -135,6 +135,7 @@ class TestGraphics(BaseTest):
# Test load query # Test load query
self.ae(l('abcd', s=1, v=1, a='q'), 'OK') self.ae(l('abcd', s=1, v=1, a='q'), 'OK')
self.assertIsNone(l('abcd', s=1, v=1, a='q', q=1))
self.ae(g.image_count, 0) self.ae(g.image_count, 0)
# Test simple load # Test simple load
@ -150,6 +151,8 @@ class TestGraphics(BaseTest):
self.ae(l('mnop', m=0), 'OK') self.ae(l('mnop', m=0), 'OK')
img = g.image_for_client_id(1) img = g.image_for_client_id(1)
self.ae(img['data'], b'abcdefghijklmnop') self.ae(img['data'], b'abcdefghijklmnop')
self.ae(l('abcd', s=10, v=10, q=1), 'ENODATA:Insufficient image data: 4 < 400')
self.ae(l('abcd', s=10, v=10, q=2), None)
# Test compression # Test compression
random_data = byte_block(3 * 1024) random_data = byte_block(3 * 1024)

View File

@ -379,7 +379,7 @@ class TestParser(BaseTest):
for f in 'action delete_action transmission_type compressed'.split(): for f in 'action delete_action transmission_type compressed'.split():
k.setdefault(f, b'\0') 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' 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').split(): ' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id quiet').split():
k.setdefault(f, 0) k.setdefault(f, 0)
p = k.pop('payload', '').encode('utf-8') p = k.pop('payload', '').encode('utf-8')
k['payload_sz'] = len(p) k['payload_sz'] = len(p)
@ -400,7 +400,7 @@ class TestParser(BaseTest):
pb('\033_Gi=12\033\\', c(id=12)) 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) 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)
t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9, payload_sz=7) t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9, payload_sz=7)
t('a=t,t=d,s=100,z=9', action='t', transmission_type='d', data_width=100, z_index=9) t('a=t,t=d,s=100,z=9,q=2', action='t', transmission_type='d', data_width=100, z_index=9, quiet=2)
e(',s=1', 'Malformed GraphicsCommand control block, invalid key character: 0x2c') e(',s=1', 'Malformed GraphicsCommand control block, invalid key character: 0x2c')
e('W=1', 'Malformed GraphicsCommand control block, invalid key character: 0x57') e('W=1', 'Malformed GraphicsCommand control block, invalid key character: 0x57')
e('1=1', 'Malformed GraphicsCommand control block, invalid key character: 0x31') e('1=1', 'Malformed GraphicsCommand control block, invalid key character: 0x31')