Repository navigation
terminal/kitty: fix various graphics deletion mismatches with spec - #13895
Merged
Merged
Conversation
- Limit d=a/A to non-virtual placements that intersect the active screen. - Keep unrelated unplaced image data when processing d=A. - Make d=R delete matching unused images even when they have no placements, and default an omitted x bound to zero. - Give ED2 a separate clear path that preserves scrollback references while reclaiming every unreferenced image. References: - Spec: https://sw.kovidgoyal.net/kitty/graphics-protocol/#deleting-images - Delete reference implementation: https://github.com/kovidgoyal/kitty/blob/0ecb10d158943e971e5254c554c2a58f1fcc79fe/kitty/graphics.c#L2114-L2363 - ED2 reference implementation: https://github.com/kovidgoyal/kitty/blob/0ecb10d158943e971e5254c554c2a58f1fcc79fe/kitty/screen.c#L2919-L2958
Contributor
Author
|
Verification: #!/bin/sh
set -eu
# Kitty graphics protocol deletion rules:
# https://sw.kovidgoyal.net/kitty/graphics-protocol/#deleting-images
# Pinned Kitty reference implementation:
# https://github.com/kovidgoyal/kitty/blob/0ecb10d158943e971e5254c554c2a58f1fcc79fe/kitty/graphics.c#L2249-L2363
# Pinned Kitty ED2 implementation:
# https://github.com/kovidgoyal/kitty/blob/0ecb10d158943e971e5254c554c2a58f1fcc79fe/kitty/screen.c#L2919-L2958
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required" >&2
exit 1
fi
if [ -n "${TMUX-}" ]; then
echo "warning: run this directly in Kitty/Ghostty; tmux may filter graphics replies" >&2
fi
echo "warning: use a disposable terminal tab; this test clears the screen and visible graphics" >&2
python3 - <<'PY'
import base64
import os
import select
import sys
import termios
import time
import tty
ESC = b"\x1b"
ST = ESC + b"\\"
tty_fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY)
saved = termios.tcgetattr(tty_fd)
results = []
def write(data):
os.write(tty_fd, data)
def text(value):
write(value.encode("utf-8"))
def graphics(control, payload=b""):
write(ESC + b"_G" + control.encode("ascii") + b";" + payload + ST)
def drain():
while select.select([tty_fd], [], [], 0)[0]:
os.read(tty_fd, 4096)
def request(control, payload=b"", timeout=2.0):
drain()
graphics(control, payload)
deadline = time.monotonic() + timeout
response = bytearray()
while time.monotonic() < deadline:
ready = select.select([tty_fd], [], [], deadline - time.monotonic())[0]
if not ready:
break
response.extend(os.read(tty_fd, 4096))
if ST in response:
break
return bytes(response)
def fire(control):
drain()
graphics(control)
time.sleep(0.05)
drain()
def transmit(image_id, rgb):
payload = base64.b64encode(bytes(rgb))
response = request(f"a=t,f=24,s=1,v=1,i={image_id}", payload)
if b";OK" not in response:
raise RuntimeError(
f"image transmission {image_id} failed or timed out: {response!r}"
)
def place(image_id, placement_id):
return request(
f"a=p,i={image_id},p={placement_id},c=4,r=1,C=1"
)
def record(name, passed, response):
results.append((name, passed, response))
status = "PASS" if passed else "FAIL"
# Never write a decoded terminal reply back to the terminal: ESC is valid
# ASCII, so doing so would execute the reply as a fresh graphics command.
detail = repr(response) if response else "no response"
text(f"{status}: {name} ({detail})\r\n")
try:
tty.setraw(tty_fd)
terminal = os.environ.get("TERM_PROGRAM", "unknown terminal")
text(f"KGP-07 conformance checks in {terminal}\r\n\r\n")
# d=A selects visible placements. An unrelated image with no placement
# must remain available for a later placement.
image_id = 7000701
transmit(image_id, (0, 255, 0))
fire("a=d,d=A")
response = place(image_id, 1)
record("d=A preserves unrelated unplaced image data", b";OK" in response, response)
fire(f"a=d,d=I,i={image_id}")
# The uppercase range action names images, so it must free matching data
# even if the image has no placement.
# Use the smallest valid client image id so the omitted x bound selects
# only this id (0..1), rather than sweeping a broad range in the terminal.
image_id = 1
transmit(image_id, (255, 0, 0))
fire(f"a=d,d=R,y={image_id}")
response = place(image_id, 1)
record(
"d=R defaults omitted x to zero and frees unplaced data",
b"ENOENT" in response,
response,
)
rows = max(os.get_terminal_size(tty_fd).lines, 2)
# A placement wholly in scrollback is not selected by d=A. Re-placing the
# same image verifies that the preserved scrollback reference kept its data
# alive. The original blue placement is also left in scrollback for visual
# inspection after the checks finish.
image_id = 7000703
write(ESC + b"[2J" + ESC + b"[H")
text("BLUE scrollback marker (d=A)\r\n")
transmit(image_id, (0, 96, 255))
response = place(image_id, 1)
if b";OK" not in response:
raise RuntimeError(f"initial blue placement failed: {response!r}")
text("\r\n" * (rows + 1))
fire("a=d,d=A")
response = place(image_id, 2)
record("d=A preserves a wholly scrolled-back placement", b";OK" in response, response)
fire(f"a=d,d=i,i={image_id},p=2")
# ED2 likewise clears visible graphics without deleting a placement that
# has already moved into scrollback. The original magenta placement is
# retained for visual inspection.
image_id = 7000704
write(ESC + b"[2J" + ESC + b"[H")
text("MAGENTA scrollback marker (ED2)\r\n")
transmit(image_id, (255, 0, 255))
response = place(image_id, 1)
if b";OK" not in response:
raise RuntimeError(f"initial magenta placement failed: {response!r}")
text("\r\n" * (rows + 1))
unplaced_id = 7000705
transmit(unplaced_id, (255, 255, 0))
write(ESC + b"[2J" + ESC + b"[H")
response = place(image_id, 2)
record("ED2 preserves a wholly scrolled-back placement", b";OK" in response, response)
fire(f"a=d,d=i,i={image_id},p=2")
response = place(unplaced_id, 1)
record("ED2 reclaims unrelated unplaced image data", b"ENOENT" in response, response)
fire(f"a=d,d=I,i={unplaced_id}")
passed = sum(1 for _, ok, _ in results if ok)
text(f"\r\nSummary: {passed}/{len(results)} checks passed.\r\n")
text("Scroll up: the original BLUE and MAGENTA markers/images should still be present.\r\n")
text("Run this once in Kitty and once in the Ghostty build under test.\r\n")
if passed != len(results):
sys.exit(1)
finally:
termios.tcsetattr(tty_fd, termios.TCSADRAIN, saved)
os.close(tty_fd)
PY |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
References: