diff options
| author | user <user@clank> | 2026-08-14 23:32:44 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-14 23:32:44 +0200 |
| commit | 7acf78ba9cb24b2fb93dca6163358d4cf69e73b2 (patch) | |
| tree | 51d7f6fa71b82b2323c795aedbdcd7d3ddb41e1f /tests | |
| parent | sl0ppty: re-emit what the program meant, not what it covers (diff) | |
| download | slosh-7acf78ba9cb24b2fb93dca6163358d4cf69e73b2.tar.gz slosh-7acf78ba9cb24b2fb93dca6163358d4cf69e73b2.tar.xz slosh-7acf78ba9cb24b2fb93dca6163358d4cf69e73b2.zip | |
sl0ppty: the graphics stream gives the cursor back
Reported as "the cursor is not rendered in zsh" in a newly spawned pane. It
was not about zsh, or about that pane: it was about the pane next to it,
which was animating an image.
Placing an image means parking the cursor on its target cell first, and the
graphics bytes go out *after* the cell diff -- deliberately, so a repainted
cell cannot land on top of a placement. That is also after the frame has put
the real cursor where the focused pane wants it. So with any image on screen,
the last instruction the terminal received every frame was "go to wherever
that image is", and the cursor sat there: in another pane, moving about with
the picture. With a bouncing DVD logo next door it is quite a sight.
gfx_begin() now opens the stream with DECSC and gfx_flush() closes it with
DECRC, so the whole thing is cursor-neutral no matter what it contains. The
save has to be written at the start of the frame rather than at flush time,
because transmissions are emitted as images are recorded and are already in
the buffer by then. A frame with nothing to say drops both and still emits
not one byte, which the test asserts, because a save/restore pair on every
idle frame is exactly the sort of cost that creeps in unnoticed.
Two tests: that the stream is bracketed (and that it really does move the
cursor, or the assertion proves nothing), and that an idle frame is empty.
One C trap worth the comment it now has: "\x1b7" is a *single* hex escape,
0x1b7, not ESC followed by 7. The compiler catches that one because it is out
of range -- and would not catch "\x1b1", which compiles quietly into one
wrong byte. Escape then digit is always two strings.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_graphics.py | 25 |
1 files changed, 25 insertions, 0 deletions
diff --git a/tests/test_graphics.py b/tests/test_graphics.py index 3f99633..241498e 100644 --- a/tests/test_graphics.py +++ b/tests/test_graphics.py @@ -175,6 +175,29 @@ def test_a_scaled_image_keeps_the_cell_count_it_asked_for(): place and "c=6" in place[0] and "r=2" in place[0], str(place)) +def test_the_graphics_stream_leaves_the_cursor_alone(): + """Placing an image parks the cursor on the target cell, and these bytes + go out after the cell diff -- so without a save/restore the last thing the + terminal hears every frame is "go to wherever that image is", and the real + cursor sits there instead of in the pane you are typing in. Reported as + "the cursor is not rendered in my shell".""" + with Session(sends_image(), cols=44, rows=10) as s: + s.settle(200) + raw = s.api("graphics", format="bytes")["bytes"] + check("it moves the cursor at all (or this proves nothing)", + "H" in raw, repr(raw[:80])) + check("saved first", raw.startswith("\x1b7"), repr(raw[:8])) + check("and restored last", raw.endswith("\x1b8"), repr(raw[-8:])) + + +def test_a_frame_with_no_images_emits_nothing(): + """The save/restore must not become a cost every idle frame pays.""" + with Session(["/bin/sh", "-c", "stty raw -echo; cat"], cols=44, rows=10) as s: + s.settle(150) + check("not one byte", s.api("graphics", format="bytes")["bytes"] == "", + repr(s.api("graphics", format="bytes")["bytes"])) + + def test_ids_cannot_collide_between_panes(): """Two panes, both using image id 7. They must not become one image.""" with Session(sends_image(image_id=7), cols=90, rows=12) as s: @@ -285,6 +308,8 @@ if __name__ == "__main__": test_a_placement_with_no_offset_emits_none() test_a_natural_image_is_never_rescaled_as_it_moves() test_a_scaled_image_keeps_the_cell_count_it_asked_for() + test_the_graphics_stream_leaves_the_cursor_alone() + test_a_frame_with_no_images_emits_nothing() test_ids_cannot_collide_between_panes() test_placement_follows_the_layout() test_cropped_at_the_pane_edge() |
