aboutsummaryrefslogtreecommitdiffstats
path: root/src/graphics.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 23:32:44 +0200
committeruser <user@clank>2026-08-14 23:32:44 +0200
commit7acf78ba9cb24b2fb93dca6163358d4cf69e73b2 (patch)
tree51d7f6fa71b82b2323c795aedbdcd7d3ddb41e1f /src/graphics.c
parentsl0ppty: re-emit what the program meant, not what it covers (diff)
downloadslosh-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 'src/graphics.c')
-rw-r--r--src/graphics.c36
1 files changed, 36 insertions, 0 deletions
diff --git a/src/graphics.c b/src/graphics.c
index d3fc07c..bb8edd4 100644
--- a/src/graphics.c
+++ b/src/graphics.c
@@ -93,6 +93,16 @@ void gfx_begin(graphics_t *g) {
for (size_t i = 0; i < g->nplaces; i++) g->places[i].live = false;
g->len = 0;
if (g->out) g->out[0] = 0;
+ /* Saved here rather than in gfx_flush(): transmissions are emitted as
+ * images are recorded, so by flush time they are already in the buffer and
+ * a save written then would not cover them. See gfx_flush() for why the
+ * whole stream has to be cursor-neutral.
+ *
+ * Split deliberately: "\x1b7" is a *single* hex escape, 0x1b7, which is out
+ * of range for a char -- the compiler catches that one, but the same shape
+ * with a digit that stays in range ("\x1b1") compiles and emits one wrong
+ * byte. Escape then digit is always two strings. */
+ out_str(g, "\x1b" "7"); /* DECSC */
}
static gfx_image_t *img_find(graphics_t *g, uint32_t pane, uint32_t src_id) {
@@ -180,6 +190,23 @@ void gfx_place(graphics_t *g, const gfx_req_t *req) {
}
char *gfx_flush(graphics_t *g, size_t *out_len) {
+ /* This stream must leave the cursor exactly where it found it.
+ *
+ * Placing an image means parking the cursor on the target cell first, and
+ * these bytes go out *after* the cell diff (so a repainted cell cannot land
+ * on top of a placement) -- which is also after the frame has put the real
+ * cursor where the focused pane wants it. Without the save/restore around
+ * this, the last thing the terminal is told each frame is "go to wherever
+ * the last image is", and the cursor sits there instead: in another pane,
+ * moving about with the picture. It looks exactly like the shell you are
+ * typing into has lost its cursor, which is how it was reported.
+ *
+ * DECSC/DECRC rather than tracking the position here, because the frame's
+ * cursor is the screen's business and this file should not have to know it.
+ * The save is written by gfx_begin(); an empty frame drops both below and
+ * still costs nothing. */
+ const size_t before = 2; /* the DECSC gfx_begin() wrote */
+
/* Placements that were on screen last frame and are not now must be told to
* go away; a terminal will happily keep drawing an image nobody owns. */
size_t keep = 0;
@@ -217,6 +244,15 @@ char *gfx_flush(graphics_t *g, size_t *out_len) {
out_str(g, "\x1b\\");
}
+ if (g->len == before) {
+ /* Nothing happened this frame: drop the save too, so an idle session with
+ * no images on screen emits not one byte. */
+ g->len = 0;
+ if (g->out) g->out[0] = 0;
+ } else {
+ out_str(g, "\x1b" "8"); /* DECRC */
+ }
+
*out_len = g->len;
return g->out; /* borrowed: valid until the next gfx_begin */
}