From af342292b8c777a1e831acc9c05a33738a9c3eb6 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 29 Apr 2026 11:52:39 +0200 Subject: Track image placements per window A buffer can be visible in more than one window. With one last-row/last-col and one placement ID per overlay, refreshing the second window overwrites the placement state for the first. After C-x 0, the closed pane's terminal image can no longer be addressed and remains as an artifact. Record image placement geometry and placement IDs per live window. Refresh and cleanup now delete the placement for the specific window, while existing last-row/last-col properties mirror the window currently being refreshed for backend compatibility. --- kitty-graphics.el | 165 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 122 insertions(+), 43 deletions(-) diff --git a/kitty-graphics.el b/kitty-graphics.el index 210d52b..04158ff 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -1494,6 +1494,19 @@ to prevent flicker." (lambda (win) (with-current-buffer (window-buffer win) (when kitty-gfx--overlays + ;; A single overlay can be visible in multiple windows showing + ;; the same buffer. Its legacy last-row/last-col properties + ;; mirror the placement for the window currently being + ;; refreshed; per-window placement tracking below keeps the + ;; individual terminal regions deletable when one window later + ;; disappears. + (dolist (ov kitty-gfx--overlays) + (let ((placement (kitty-gfx--image-placement ov win))) + (unless (overlay-get ov 'kitty-gfx-heading) + (overlay-put ov 'kitty-gfx-last-row + (plist-get (cdr placement) :row)) + (overlay-put ov 'kitty-gfx-last-col + (plist-get (cdr placement) :col))))) ;; Prune dead overlays (overlay-buffer returns nil) (let ((before (length kitty-gfx--overlays))) (setq kitty-gfx--overlays @@ -1534,10 +1547,13 @@ refresh based on overlay type." ;; OSC 66 emission happens in phase 2 (kitty-gfx--emit-heading-overlays). (kitty-gfx--refresh-heading-overlay ov win win-bottom) ;; Image overlay refresh - (let ((pos (kitty-gfx--overlay-screen-pos ov win)) - (rows (overlay-get ov 'kitty-gfx-rows)) - (last-row (overlay-get ov 'kitty-gfx-last-row)) - (last-col (overlay-get ov 'kitty-gfx-last-col))) + (let* ((pos (kitty-gfx--overlay-screen-pos ov win)) + (rows (overlay-get ov 'kitty-gfx-rows)) + (cols (overlay-get ov 'kitty-gfx-cols)) + (placement (kitty-gfx--image-placement ov win)) + (placement-data (cdr placement)) + (last-row (plist-get placement-data :row)) + (last-col (plist-get placement-data :col))) (let ((pid (overlay-get ov 'kitty-gfx-pid)) (id (overlay-get ov 'kitty-gfx-id))) (if (and pos @@ -1560,15 +1576,19 @@ refresh based on overlay type." new-row new-col) (overlay-put ov 'kitty-gfx-last-row new-row) (overlay-put ov 'kitty-gfx-last-col new-col) + (kitty-gfx--record-image-placement ov win new-row new-col cols rows pid) + (setq placement (kitty-gfx--image-placement ov win) + pid (plist-get (cdr placement) :pid)) (funcall (kitty-gfx--backend-fn 'place) - ov id pid (overlay-get ov 'kitty-gfx-cols) rows new-row new-col))) - ;; Not visible or overflows — delete if was placed - (when last-row - (kitty-gfx--log "refresh-ov: pid=%d hiding (was row=%d col=%d)" - pid last-row last-col) + ov id pid cols rows new-row new-col))) + ;; Not visible or overflows — delete if was placed in this window + (when placement + (kitty-gfx--log "refresh-ov: pid=%d hiding in win=%s (was row=%d col=%d)" + pid win last-row last-col) + (kitty-gfx--delete-image-placement ov placement) + (kitty-gfx--forget-image-placement ov win) (overlay-put ov 'kitty-gfx-last-row nil) - (overlay-put ov 'kitty-gfx-last-col nil) - (funcall (kitty-gfx--backend-fn 'delete) ov id pid))))))) + (overlay-put ov 'kitty-gfx-last-col nil))))))) (defun kitty-gfx--refresh-heading-overlay (ov win win-bottom) "Refresh heading overlay OV in WIN. @@ -1663,15 +1683,9 @@ then invalidates position caches and schedules a refresh." (kitty-gfx--erase-heading ov) (overlay-put ov 'kitty-gfx-last-row nil) (overlay-put ov 'kitty-gfx-last-col nil)) - ;; Image overlay — delete terminal placement - (let ((id (overlay-get ov 'kitty-gfx-id)) - (pid (overlay-get ov 'kitty-gfx-pid))) - (if (overlay-get ov 'kitty-gfx-last-row) - (when (and id pid) - (kitty-gfx--delete-placement id pid)) - (kitty-gfx--log "on-buffer-change: ov pid=%s has no last-row (never placed or already cleaned)" pid))) - (overlay-put ov 'kitty-gfx-last-row nil) - (overlay-put ov 'kitty-gfx-last-col nil)))))))) + ;; Image overlay — delete all terminal placements, including + ;; multiple windows that were showing this buffer. + (kitty-gfx--delete-image-placements ov)))))))) ;; Reset cache for visible buffers so they re-place correctly. ;; Heading overlays preserve cache (same rationale as on-window-change). (dolist (buf (buffer-list)) @@ -1679,6 +1693,7 @@ then invalidates position caches and schedules a refresh." (dolist (ov kitty-gfx--overlays) (when (and (overlay-buffer ov) (not (overlay-get ov 'kitty-gfx-heading))) + (overlay-put ov 'kitty-gfx-placements nil) (overlay-put ov 'kitty-gfx-last-row nil) (overlay-put ov 'kitty-gfx-last-col nil))))) ;; Longer debounce: cancel any fast leading-edge cooldown and @@ -1705,12 +1720,13 @@ settling to one)." kitty-gfx--cell-pixel-height nil) ;; Delete image placements before clearing their cached positions. ;; Window splits/resizes can move an image from the middle of the old - ;; window to the center of the new pane(s). Some terminals do not - ;; reliably erase the old direct placement when it is re-placed at a - ;; different geometry, and Sixel is stateless and must be explicitly - ;; overwritten. If we clear the cache first, the backend no longer - ;; knows where to delete/erase the old placement, leaving artifacts - ;; over newly-created window separators. + ;; window to the center of the new pane(s). A buffer can also be shown + ;; in multiple windows, and closing one of those windows leaves terminal + ;; pixels that no remaining window can discover from `posn-at-point'. + ;; Therefore image placements are tracked and deleted per window before + ;; the cache is reset. Some terminals do not reliably erase an old + ;; direct placement when it is re-placed at a different geometry, and + ;; Sixel is stateless and must be explicitly overwritten. ;; ;; Heading overlays PRESERVE their cache — the refresh cycle needs ;; old→new position comparison to erase multicell blocks properly. @@ -1721,17 +1737,7 @@ settling to one)." (dolist (ov kitty-gfx--overlays) (when (and (overlay-buffer ov) (not (overlay-get ov 'kitty-gfx-heading))) - (let ((id (overlay-get ov 'kitty-gfx-id)) - (pid (overlay-get ov 'kitty-gfx-pid))) - (when (and id pid kitty-gfx--active-backend - (overlay-get ov 'kitty-gfx-last-row)) - (condition-case err - (funcall (kitty-gfx--backend-fn 'delete) ov id pid) - (error - (kitty-gfx--log "on-window-change: error deleting placement: %s" - (error-message-string err)))))) - (overlay-put ov 'kitty-gfx-last-row nil) - (overlay-put ov 'kitty-gfx-last-col nil))))) + (kitty-gfx--delete-image-placements ov))))) (kitty-gfx--sync-end)) ;; Longer debounce: cancel any fast leading-edge cooldown and ;; schedule a 0.1s delayed refresh to let window layout settle. @@ -1924,6 +1930,82 @@ delete step, avoiding visual glitches in some terminals." image-id pid cols rows beg end (buffer-name) (length kitty-gfx--overlays)) ov)) +(defun kitty-gfx--image-placement (ov win) + "Return OV's recorded image placement for WIN, or nil." + (assq win (overlay-get ov 'kitty-gfx-placements))) + +(defun kitty-gfx--record-image-placement (ov win row col cols rows pid) + "Record that OV is placed in WIN at ROW COL with COLS ROWS and PID." + (unless (kitty-gfx--image-placement ov win) + (setq pid (kitty-gfx--alloc-placement-id))) + (let ((placements (assq-delete-all win (copy-sequence + (overlay-get ov 'kitty-gfx-placements))))) + (overlay-put ov 'kitty-gfx-placements + (cons (cons win (list :row row :col col + :cols cols :rows rows + :pid pid)) + placements)))) + +(defun kitty-gfx--forget-image-placement (ov win) + "Forget OV's recorded image placement for WIN." + (overlay-put ov 'kitty-gfx-placements + (assq-delete-all win (copy-sequence + (overlay-get ov 'kitty-gfx-placements))))) + +(defun kitty-gfx--delete-image-placement (ov placement) + "Delete one recorded image PLACEMENT for OV." + (let* ((data (cdr placement)) + (id (overlay-get ov 'kitty-gfx-id)) + (pid (plist-get data :pid)) + (row (plist-get data :row)) + (col (plist-get data :col)) + (cols (plist-get data :cols)) + (rows (plist-get data :rows)) + (old-row (overlay-get ov 'kitty-gfx-last-row)) + (old-col (overlay-get ov 'kitty-gfx-last-col)) + (old-cols (overlay-get ov 'kitty-gfx-cols)) + (old-rows (overlay-get ov 'kitty-gfx-rows))) + (when (and id pid row col cols rows kitty-gfx--active-backend) + ;; Sixel deletion is position-based and reads these properties from OV; + ;; Kitty deletion ignores them and deletes by PID. Temporarily bind the + ;; recorded geometry so both backends can share the same helper. + (unwind-protect + (progn + (overlay-put ov 'kitty-gfx-last-row row) + (overlay-put ov 'kitty-gfx-last-col col) + (overlay-put ov 'kitty-gfx-cols cols) + (overlay-put ov 'kitty-gfx-rows rows) + (funcall (kitty-gfx--backend-fn 'delete) ov id pid)) + (overlay-put ov 'kitty-gfx-last-row old-row) + (overlay-put ov 'kitty-gfx-last-col old-col) + (overlay-put ov 'kitty-gfx-cols old-cols) + (overlay-put ov 'kitty-gfx-rows old-rows))))) + +(defun kitty-gfx--delete-image-placements (ov) + "Delete all recorded terminal placements for image overlay OV." + (let ((placements (overlay-get ov 'kitty-gfx-placements))) + (if placements + (dolist (placement placements) + (condition-case err + (kitty-gfx--delete-image-placement ov placement) + (error + (kitty-gfx--log "delete-image-placements: error: %s" + (error-message-string err))))) + ;; Backward-compatible fallback for overlays created before per-window + ;; placement tracking or for callers that only populated last-row/col. + (let ((id (overlay-get ov 'kitty-gfx-id)) + (pid (overlay-get ov 'kitty-gfx-pid))) + (when (and id pid kitty-gfx--active-backend + (overlay-get ov 'kitty-gfx-last-row)) + (condition-case err + (funcall (kitty-gfx--backend-fn 'delete) ov id pid) + (error + (kitty-gfx--log "delete-image-placements: fallback error: %s" + (error-message-string err)))))))) + (overlay-put ov 'kitty-gfx-placements nil) + (overlay-put ov 'kitty-gfx-last-row nil) + (overlay-put ov 'kitty-gfx-last-col nil)) + (defun kitty-gfx--delete-placement (id pid) "Delete a specific placement PID of image ID from terminal. Uses d=i (lowercase) to remove the placement but keep stored image @@ -1939,17 +2021,14 @@ the placement ID can be reused by a subsequent overlay (avoids visual glitches from delete+re-place sequences in some terminals)." (let ((id (overlay-get ov 'kitty-gfx-id)) (pid (overlay-get ov 'kitty-gfx-pid))) + (when keep-placement + (overlay-put ov 'kitty-gfx-placements nil)) (kitty-gfx--log "remove-overlay: id=%s pid=%s keep=%s buf=%s" id pid keep-placement (when (overlay-buffer ov) (buffer-name (overlay-buffer ov)))) (when (overlay-buffer ov) (unless keep-placement - (condition-case err - (when (and id pid kitty-gfx--active-backend) - (funcall (kitty-gfx--backend-fn 'delete) ov id pid)) - (error - (kitty-gfx--log "remove-overlay: error deleting placement: %s" - (error-message-string err))))) + (kitty-gfx--delete-image-placements ov)) (delete-overlay ov)) (setq kitty-gfx--overlays (delq ov kitty-gfx--overlays)) (kitty-gfx--log "remove-overlay: done (remaining=%d)" (length kitty-gfx--overlays)))) -- cgit v1.3.1