mirror of
https://github.com/tmux/tmux.git
synced 2026-10-10 12:14:43 +00:00
Fix Kitty image cursor movement, replies and screen clearing.
This commit is contained in:
@@ -1095,6 +1095,7 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel,
|
|||||||
uint64_t canvas_width, canvas_height;
|
uint64_t canvas_width, canvas_height;
|
||||||
struct image *im = NULL, *source;
|
struct image *im = NULL, *source;
|
||||||
struct kitty_source *stored;
|
struct kitty_source *stored;
|
||||||
|
int error;
|
||||||
|
|
||||||
if (kc == NULL) {
|
if (kc == NULL) {
|
||||||
kc = xcalloc(1, sizeof *kc);
|
kc = xcalloc(1, sizeof *kc);
|
||||||
@@ -1114,24 +1115,23 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel,
|
|||||||
ks->medium = 'd';
|
ks->medium = 'd';
|
||||||
}
|
}
|
||||||
ks->more = 0;
|
ks->more = 0;
|
||||||
|
error = kitty_control(ks, buf, controllen);
|
||||||
|
if (ks->image_id == 0)
|
||||||
|
ks->placement_id = 0;
|
||||||
result->image_id = ks->image_id;
|
result->image_id = ks->image_id;
|
||||||
result->quiet = ks->quiet;
|
result->quiet = ks->quiet;
|
||||||
result->action = ks->action;
|
result->action = ks->action;
|
||||||
result->delete = ks->delete;
|
result->delete = ks->delete;
|
||||||
result->placement_id = ks->placement_id;
|
result->placement_id = ks->placement_id;
|
||||||
result->z = ks->z;
|
result->z = ks->z;
|
||||||
if (kitty_control(ks, buf, controllen) != 0 ||
|
if (error != 0)
|
||||||
ks->medium != 'd' ||
|
|
||||||
(payloadlen != 0 &&
|
|
||||||
kitty_append(ks, semi + 1, payloadlen) != 0))
|
|
||||||
goto fail;
|
goto fail;
|
||||||
|
if (ks->medium != 'd')
|
||||||
result->image_id = ks->image_id;
|
goto fail;
|
||||||
result->quiet = ks->quiet;
|
if (payloadlen != 0) {
|
||||||
result->action = ks->action;
|
if (kitty_append(ks, semi + 1, payloadlen) != 0)
|
||||||
result->delete = ks->delete;
|
goto fail;
|
||||||
result->placement_id = ks->placement_id;
|
}
|
||||||
result->z = ks->z;
|
|
||||||
if (ks->more) {
|
if (ks->more) {
|
||||||
kc->transfer = ks;
|
kc->transfer = ks;
|
||||||
result->status = KITTY_PARSE_MORE;
|
result->status = KITTY_PARSE_MORE;
|
||||||
@@ -1257,7 +1257,9 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel,
|
|||||||
result->status = KITTY_PARSE_OK;
|
result->status = KITTY_PARSE_OK;
|
||||||
if (ks->action != 'q')
|
if (ks->action != 'q')
|
||||||
result->replace_id = kitty_source_set(kc, ks->image_id, source);
|
result->replace_id = kitty_source_set(kc, ks->image_id, source);
|
||||||
if (ks->action == 'T' && !ks->virtual) {
|
if (ks->action == 'q')
|
||||||
|
im = NULL;
|
||||||
|
else if (ks->action == 'T' && !ks->virtual) {
|
||||||
im = kitty_place_image(source, ks, xpixel, ypixel);
|
im = kitty_place_image(source, ks, xpixel, ypixel);
|
||||||
if (im == NULL)
|
if (im == NULL)
|
||||||
result->status = KITTY_PARSE_ERROR;
|
result->status = KITTY_PARSE_ERROR;
|
||||||
|
|||||||
8
image.c
8
image.c
@@ -1602,6 +1602,7 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg,
|
|||||||
u_int cx = s->cx, cy = s->cy;
|
u_int cx = s->cx, cy = s->cy;
|
||||||
u_int x, y, i, run, sx, sy, lines, origin_y = 0;
|
u_int x, y, i, run, sx, sy, lines, origin_y = 0;
|
||||||
u_int hist_origin_y, region_height, remaining, chunk;
|
u_int hist_origin_y, region_height, remaining, chunk;
|
||||||
|
u_int cursor_x = 0;
|
||||||
|
|
||||||
sx = im->sx;
|
sx = im->sx;
|
||||||
if (sx > screen_size_x(s) - cx)
|
if (sx > screen_size_x(s) - cx)
|
||||||
@@ -1678,8 +1679,11 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg,
|
|||||||
}
|
}
|
||||||
image_store_prune(gd->images);
|
image_store_prune(gd->images);
|
||||||
image_redraw_area(ctx, cx, cy, sx, sy);
|
image_redraw_area(ctx, cx, cy, sx, sy);
|
||||||
if (!(im->flags & IMAGE_FLAG_NO_CURSOR))
|
if (!(im->flags & IMAGE_FLAG_NO_CURSOR)) {
|
||||||
screen_write_cursormove(ctx, 0, cy + sy, 0);
|
if (input == IMAGE_INPUT_KITTY)
|
||||||
|
cursor_x = cx + sx;
|
||||||
|
screen_write_cursormove(ctx, cursor_x, cy + sy, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Place an image received through SIXEL. */
|
/* Place an image received through SIXEL. */
|
||||||
|
|||||||
46
input.c
46
input.c
@@ -2803,6 +2803,30 @@ input_enter_apc(struct input_ctx *ictx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_IMAGES
|
#ifdef ENABLE_IMAGES
|
||||||
|
/* Reply to a Kitty graphics command with its image and placement IDs. */
|
||||||
|
static void
|
||||||
|
input_reply_kitty(struct input_ctx *ictx, struct kitty_parse_result *result,
|
||||||
|
const char *message)
|
||||||
|
{
|
||||||
|
if (result->quiet >= 2)
|
||||||
|
return;
|
||||||
|
if (result->quiet == 1) {
|
||||||
|
if (strcmp(message, "OK") == 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result->image_id == 0) {
|
||||||
|
if (result->action != 'q')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result->placement_id != 0) {
|
||||||
|
input_reply(ictx, 0, "\033_Gi=%u,p=%u;%s\033\\",
|
||||||
|
result->image_id, result->placement_id, message);
|
||||||
|
} else {
|
||||||
|
input_reply(ictx, 0, "\033_Gi=%u;%s\033\\", result->image_id,
|
||||||
|
message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Handle a Kitty graphics command. */
|
/* Handle a Kitty graphics command. */
|
||||||
static int
|
static int
|
||||||
input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
|
input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
|
||||||
@@ -2819,14 +2843,10 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
|
|||||||
if (result.status == KITTY_PARSE_MORE)
|
if (result.status == KITTY_PARSE_MORE)
|
||||||
return (1);
|
return (1);
|
||||||
if (result.status != KITTY_PARSE_OK) {
|
if (result.status != KITTY_PARSE_OK) {
|
||||||
if (result.quiet < 2 && result.action != '\0') {
|
if (result.status == KITTY_PARSE_MISSING)
|
||||||
if (result.status == KITTY_PARSE_MISSING)
|
input_reply_kitty(ictx, &result, "ENOENT");
|
||||||
input_reply(ictx, 0, "\033_Gi=%u;ENOENT\033\\",
|
else
|
||||||
result.image_id);
|
input_reply_kitty(ictx, &result, "EINVAL");
|
||||||
else
|
|
||||||
input_reply(ictx, 0, "\033_Gi=%u;EINVAL\033\\",
|
|
||||||
result.image_id);
|
|
||||||
}
|
|
||||||
return (1);
|
return (1);
|
||||||
}
|
}
|
||||||
if (result.replace_id != 0)
|
if (result.replace_id != 0)
|
||||||
@@ -2839,14 +2859,12 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
|
|||||||
image_write_kitty(sctx, im, ictx->cell.cell.bg,
|
image_write_kitty(sctx, im, ictx->cell.cell.bg,
|
||||||
result.image_id, result.placement_id, result.z);
|
result.image_id, result.placement_id, result.z);
|
||||||
image_free(image_get_id(im));
|
image_free(image_get_id(im));
|
||||||
if (result.quiet == 0 && result.image_id != 0)
|
} else if (result.action == 'd') {
|
||||||
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", result.image_id);
|
|
||||||
} else if (result.action == 'd')
|
|
||||||
image_clear_kitty(sctx, result.delete, result.image_id,
|
image_clear_kitty(sctx, result.delete, result.image_id,
|
||||||
result.placement_id, result.z);
|
result.placement_id, result.z);
|
||||||
else if ((result.action == 't' || result.action == 'q' ||
|
}
|
||||||
result.action == 'u') && result.quiet == 0)
|
if (result.action != 'd')
|
||||||
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", result.image_id);
|
input_reply_kitty(ictx, &result, "OK");
|
||||||
return (1);
|
return (1);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
41
regress/image-kitty-clear.sh
Normal file
41
regress/image-kitty-clear.sh
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Erasing a line preserves Kitty graphics; erasing the screen removes them.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
LC_ALL=C
|
||||||
|
export TERM LC_ALL
|
||||||
|
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
TMUX="$TEST_TMUX -Lkitty-clear-inner$$ -f/dev/null"
|
||||||
|
TMUX2="$TEST_TMUX -Lkitty-clear-outer$$ -f/dev/null"
|
||||||
|
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15
|
||||||
|
|
||||||
|
$TMUX new-session -d -x 10 -y 4 "
|
||||||
|
printf '\033_Ga=T,q=2,f=24,s=1,v=1,c=1,r=1,C=1;////\033\\'
|
||||||
|
read line
|
||||||
|
printf '\033[H\033[2K'
|
||||||
|
read line
|
||||||
|
printf '\033[H\033[2J'
|
||||||
|
sleep 30" || exit 1
|
||||||
|
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
|
||||||
|
$TMUX set -g status off || exit 1
|
||||||
|
$TMUX set -g scroll-on-clear off || exit 1
|
||||||
|
$TMUX set -as terminal-features ',*:sixel@' || exit 1
|
||||||
|
$TMUX set -as terminal-features ',*:kitty@' || exit 1
|
||||||
|
$TMUX set -as terminal-features ',*:RGB' || exit 1
|
||||||
|
$TMUX2 new-session -d -x 10 -y 4 "$TMUX attach-session" || exit 1
|
||||||
|
$TMUX2 set -g status off || exit 1
|
||||||
|
sleep 1
|
||||||
|
$TMUX2 capture-pane -peS0 -E0 | grep -q '48;2;255;255;255m' || exit 1
|
||||||
|
|
||||||
|
$TMUX send-keys Enter || exit 1
|
||||||
|
sleep 1
|
||||||
|
$TMUX2 capture-pane -peS0 -E0 | grep -q '48;2;255;255;255m' || exit 1
|
||||||
|
|
||||||
|
$TMUX send-keys Enter || exit 1
|
||||||
|
sleep 1
|
||||||
|
[ -z "$($TMUX2 capture-pane -peS0 -E3)" ] || exit 1
|
||||||
|
|
||||||
|
exit 0
|
||||||
130
regress/image-kitty-protocol.sh
Normal file
130
regress/image-kitty-protocol.sh
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Kitty cursor movement, acknowledgements and query isolation.
|
||||||
|
|
||||||
|
PATH=/bin:/usr/bin
|
||||||
|
TERM=screen
|
||||||
|
LC_ALL=C.UTF-8
|
||||||
|
export TERM LC_ALL
|
||||||
|
|
||||||
|
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||||
|
export TEST_TMUX
|
||||||
|
command -v python3 >/dev/null || exit 0
|
||||||
|
|
||||||
|
python3 - <<'PY'
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
tmux = [os.environ['TEST_TMUX'], '-u', '-Limage-protocol' + str(os.getpid()),
|
||||||
|
'-f/dev/null']
|
||||||
|
|
||||||
|
def run(*args):
|
||||||
|
return subprocess.check_output(tmux + list(args), text=True).strip()
|
||||||
|
|
||||||
|
def graphics(control, payload=''):
|
||||||
|
return '\033_G' + control + (';' + payload if payload else '') + '\033\\'
|
||||||
|
|
||||||
|
# Read until a following device-attributes reply, including when q=2 is used.
|
||||||
|
reader = '''
|
||||||
|
import json, os, select, sys, termios, time, tty
|
||||||
|
command, output = sys.argv[1:]
|
||||||
|
tty.setraw(0)
|
||||||
|
os.write(1, command.encode() + b'\\x1b[c')
|
||||||
|
reply = b''
|
||||||
|
deadline = time.monotonic() + 3
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if select.select([0], [], [], 0.1)[0]:
|
||||||
|
reply += os.read(0, 4096)
|
||||||
|
if b'\\x1b[?' in reply and reply.endswith(b'c'):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise SystemExit('missing device-attributes reply')
|
||||||
|
with open(output, 'w') as f:
|
||||||
|
json.dump(reply.split(b'\\x1b[?')[0].decode(), f)
|
||||||
|
time.sleep(30)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def check(name, command, cursor, expected='', text=None):
|
||||||
|
output = directory / name
|
||||||
|
pane_command = 'python3 ' + shlex.quote(str(helper)) + ' ' + \
|
||||||
|
shlex.quote(command) + ' ' + shlex.quote(str(output))
|
||||||
|
pane = run('new-window', '-d', '-P', '-F', '#{pane_id}', pane_command)
|
||||||
|
try:
|
||||||
|
deadline = time.monotonic() + 5
|
||||||
|
while not output.exists():
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
raise AssertionError(name + ': missing reply capture')
|
||||||
|
time.sleep(0.05)
|
||||||
|
reply = json.loads(output.read_text())
|
||||||
|
assert reply == expected, (name, 'reply', repr(reply), repr(expected))
|
||||||
|
actual = run('display-message', '-pt', pane, '#{cursor_x},#{cursor_y}')
|
||||||
|
assert actual == cursor, (name, 'cursor', actual, cursor)
|
||||||
|
if text is not None:
|
||||||
|
actual = run('capture-pane', '-pt', pane, '-S0', '-E0')
|
||||||
|
assert actual == text, (name, 'text', actual, text)
|
||||||
|
finally:
|
||||||
|
run('kill-window', '-t', pane)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
|
||||||
|
directory = Path(tmp)
|
||||||
|
helper = directory / 'read-reply.py'
|
||||||
|
helper.write_text(reader)
|
||||||
|
try:
|
||||||
|
run('new-session', '-d', '-x', '40', '-y', '12')
|
||||||
|
if run('display-message', '-p', '#{image_support}') == '0':
|
||||||
|
raise SystemExit(0)
|
||||||
|
origin = '\033[3;6H' # Column 6, row 3 (zero-based 5,2).
|
||||||
|
pixel = '/wAA/w=='
|
||||||
|
placement = 'a=T,q=2,f=32,s=1,v=1,c=3,r=2'
|
||||||
|
check('placement', origin + graphics(placement, pixel), '8,4')
|
||||||
|
check('no-cursor', origin + graphics(placement + ',C=1', pixel), '5,2')
|
||||||
|
check('clipped', '\033[3;39H' + graphics(placement, pixel), '39,4')
|
||||||
|
check('scrolled', '\033[12;6H' + graphics(placement, pixel), '8,11')
|
||||||
|
check('transmit', origin + graphics('a=t,q=2,f=32,s=1,v=1,i=7', pixel), '5,2')
|
||||||
|
check('virtual', origin + graphics(placement + ',U=1,i=7', pixel), '5,2')
|
||||||
|
check('put', origin + graphics('a=t,q=2,f=32,s=1,v=1,i=7', pixel) +
|
||||||
|
graphics('a=p,q=2,i=7,c=3,r=2'), '8,4')
|
||||||
|
check('chunks', graphics(placement + ',m=1', '/wAA') + origin +
|
||||||
|
graphics('m=0', '/w=='), '8,4')
|
||||||
|
check('sixel', origin + '\033Pq"1;1;1;1#0;2;100;0;0#0@\033\\', '0,3')
|
||||||
|
check('put-reply', origin + graphics('a=t,q=2,f=32,s=1,v=1,i=7', pixel) +
|
||||||
|
graphics('a=p,i=7,p=9,c=3,r=2'), '8,4', graphics('i=7,p=9', 'OK'))
|
||||||
|
check('missing-reply', origin + graphics('a=p,i=7,p=9'), '5,2',
|
||||||
|
graphics('i=7,p=9', 'ENOENT'))
|
||||||
|
check('invalid-reply', origin + graphics('a=T,i=7,p=9,f=32,s=1,v=1', '!!!!'),
|
||||||
|
'5,2', graphics('i=7,p=9', 'EINVAL'))
|
||||||
|
check('quiet-error', origin + graphics('a=T,q=2,i=7,f=32,s=1,v=1', '!!!!'), '5,2')
|
||||||
|
check('quiet-ok', origin + graphics('a=t,q=1,i=7,f=32,s=1,v=1', pixel), '5,2')
|
||||||
|
check('quiet-one-error', origin + graphics('a=t,q=1,i=7,f=32,s=1,v=1', '!!!!'),
|
||||||
|
'5,2', graphics('i=7', 'EINVAL'))
|
||||||
|
check('quiet-control-error', origin + graphics('a=t,q=2,i=7,z=bad'), '5,2')
|
||||||
|
check('anonymous', origin + graphics('a=t,f=32,s=1,v=1', pixel), '5,2')
|
||||||
|
check('anonymous-error', origin + graphics('a=t,f=32,s=1,v=1', '!!!!'), '5,2')
|
||||||
|
check('anonymous-placement', origin + graphics('a=T,p=9,f=32,s=1,v=1,c=3,r=2',
|
||||||
|
pixel), '8,4')
|
||||||
|
check('delete-no-reply', origin + graphics('a=t,q=2,i=7,f=32,s=1,v=1', pixel) +
|
||||||
|
graphics('a=d,d=I,i=7'), '5,2')
|
||||||
|
for format, raw in [(24, b'\xff\0\0'), (32, b'\xff\0\0\xff')]:
|
||||||
|
payload = base64.b64encode(zlib.compress(raw)).decode()
|
||||||
|
check('compressed-' + str(format), origin +
|
||||||
|
graphics('a=T,q=2,f=%d,s=1,v=1,c=3,r=2,o=z' % format, payload), '8,4')
|
||||||
|
check('query', origin + graphics('a=q,f=32,s=1,v=1', pixel), '5,2',
|
||||||
|
graphics('i=0', 'OK'))
|
||||||
|
check('query-not-stored', origin + graphics('a=q,q=2,i=7,f=32,s=1,v=1', pixel) +
|
||||||
|
graphics('a=p,i=7'), '5,2', graphics('i=7', 'ENOENT'))
|
||||||
|
# A query must not change an existing virtual image. Column 2 is
|
||||||
|
# valid for the original three-column placement, but not the query.
|
||||||
|
check('query-virtual', graphics(placement + ',U=1,i=7', pixel) +
|
||||||
|
graphics('a=q,q=2,U=1,i=7,f=32,s=1,v=1,c=1,r=1', pixel) +
|
||||||
|
'\033[38;2;0;0;7m\U0010eeee\u0305\u030e', '1,0', text='')
|
||||||
|
finally:
|
||||||
|
subprocess.run(tmux + ['kill-server'], stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL)
|
||||||
|
PY
|
||||||
@@ -2242,8 +2242,15 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
|
|||||||
ctx->wp != NULL &&
|
ctx->wp != NULL &&
|
||||||
options_get_number(ctx->wp->options, "scroll-on-clear"))
|
options_get_number(ctx->wp->options, "scroll-on-clear"))
|
||||||
grid_view_clear_history(s->grid, bg);
|
grid_view_clear_history(s->grid, bg);
|
||||||
else
|
else {
|
||||||
|
#ifdef ENABLE_IMAGES
|
||||||
|
for (y = 0; y < sy; y++) {
|
||||||
|
image_grid_free_line(s->grid,
|
||||||
|
&s->grid->linedata[s->grid->hsize + y]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
grid_view_clear(s->grid, 0, 0, sx, sy, bg);
|
grid_view_clear(s->grid, 0, 0, sx, sy, bg);
|
||||||
|
}
|
||||||
|
|
||||||
screen_write_collect_clear(ctx, 0, sy);
|
screen_write_collect_clear(ctx, 0, sy);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user