From 42f0e3ef12a885e1fdf465af0547d93df692b4bc Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 29 Apr 2026 09:33:44 +0200 Subject: Guard Sixel cleanup against nil files kitty-gfx--sixel-cleanup can be called without a file when cleanup is routed through generic backend code. In that case the old implementation passed nil to md5 while pruning cached Sixel temp files. Make the cleanup a no-op when FILE is nil. --- kitty-graphics.el | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/kitty-graphics.el b/kitty-graphics.el index d1514df..5eed0c1 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -860,13 +860,14 @@ Sixel has no placement IDs — erase by writing spaces over the region." (defun kitty-gfx--sixel-cleanup (file _image-id) "Cleanup Sixel resources for FILE." - (remhash file kitty-gfx--sixel-cache) - ;; Remove cached sixel encodings for this file - (dolist (temp-file kitty-gfx--sixel-temp-files) - (when (string-match-p (regexp-quote (md5 file)) temp-file) - (kitty-gfx--log "sixel-cleanup: deleting %s" temp-file) - (ignore-errors (delete-file temp-file)) - (setq kitty-gfx--sixel-temp-files (delete temp-file kitty-gfx--sixel-temp-files))))) + (when file + (remhash file kitty-gfx--sixel-cache) + ;; Remove cached sixel encodings for this file + (dolist (temp-file kitty-gfx--sixel-temp-files) + (when (string-match-p (regexp-quote (md5 file)) temp-file) + (kitty-gfx--log "sixel-cleanup: deleting %s" temp-file) + (ignore-errors (delete-file temp-file)) + (setq kitty-gfx--sixel-temp-files (delete temp-file kitty-gfx--sixel-temp-files)))))) (defun kitty-gfx--sixel-cleanup-all () "Cleanup all Sixel resources. -- cgit v1.3.1 From 97956b508ea435358f2b4e070158cf0c3669bec4 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 29 Apr 2026 10:40:14 +0200 Subject: Fix overlay position mapping in split windows Compute overlay screen positions in the window currently being refreshed instead of choosing an arbitrary window that shows the same buffer. This keeps placements correct when a buffer is visible in more than one pane. Also derive the terminal column from the overlay's buffer column rather than posn-col-row. In terminal Emacs, posn-col-row can describe the right edge of display-property text instead of the logical overlay start, which makes Sixel placements appear to the right of their reserved cells. --- kitty-graphics.el | 72 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/kitty-graphics.el b/kitty-graphics.el index 5eed0c1..2137867 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -1369,14 +1369,19 @@ Ignores cosmetic invisibility like hidden link brackets (`org-link')." (kitty-gfx--log "in-folded-region: pos=%d folded=%s" pos folded)) folded)) -(defun kitty-gfx--overlay-screen-pos (ov) - "Return (TERM-ROW . TERM-COL) for overlay OV, or nil if not visible. -Coordinates are 1-indexed terminal positions. +(defun kitty-gfx--overlay-screen-pos (ov &optional win) + "Return (TERM-ROW . TERM-COL) for overlay OV in WIN, or nil if hidden. +Coordinates are 1-indexed terminal positions. WIN defaults to a window +showing OV's buffer, for interactive debug helpers. Returns nil when the overlay position is outside the visible window range, inside a folded region, or not visible on screen." (let* ((buf (overlay-buffer ov)) (pos (overlay-start ov)) - (win (and buf (get-buffer-window buf)))) + (win (and buf + (or (and (window-live-p win) + (eq (window-buffer win) buf) + win) + (get-buffer-window buf))))) ;; Fast path: skip entirely if no window, no position, or ;; buffer position is outside the visible window range. ;; This avoids expensive posn-at-point and fold checks. @@ -1399,13 +1404,25 @@ range, inside a folded region, or not visible on screen." (let* ((col-row (posn-col-row win-pos)) (row (cdr col-row)) (posn-col (car col-row)) - (posn-xy (posn-x-y win-pos))) - (kitty-gfx--log "screen-pos-detail: pid=%s posn-col=%d posn-row=%d posn-xy=%S body-left=%d body-top=%d" - (overlay-get ov 'kitty-gfx-pid) posn-col row posn-xy body-left body-top) + (posn-xy (posn-x-y win-pos)) + ;; `posn-col-row' is derived from pixel coordinates and can + ;; report the position after an overlay's `display' string + ;; rather than the overlay's logical start. That is exactly + ;; the wrong edge for Sixel placement: the terminal graphic + ;; must be emitted at the top-left of the reserved cells. + ;; In terminal Emacs text cells are fixed-width, so the + ;; buffer column at POS is the reliable horizontal anchor. + (buffer-col (save-excursion + (goto-char pos) + (current-column))) + (visual-col (max 0 (- buffer-col (window-hscroll win))))) + (kitty-gfx--log "screen-pos-detail: pid=%s posn-col=%d buffer-col=%d visual-col=%d posn-row=%d posn-xy=%S body-left=%d body-top=%d" + (overlay-get ov 'kitty-gfx-pid) posn-col buffer-col + visual-col row posn-xy body-left body-top) (when col-row - (let ((result (cons (+ body-top row 1) (+ body-left posn-col 1)))) - (kitty-gfx--log "screen-pos: pid=%s pos=%d -> row=%d col=%d" - (overlay-get ov 'kitty-gfx-pid) pos + (let ((result (cons (+ body-top row 1) (+ body-left visual-col 1)))) + (kitty-gfx--log "screen-pos: pid=%s pos=%d win=%s -> row=%d col=%d" + (overlay-get ov 'kitty-gfx-pid) pos win (car result) (cdr result)) result)))))))) @@ -1492,7 +1509,7 @@ to prevent flicker." win (buffer-name) (length kitty-gfx--overlays) win-bottom) (dolist (ov kitty-gfx--overlays) (cl-incf total-overlays) - (kitty-gfx--refresh-overlay ov win-bottom) + (kitty-gfx--refresh-overlay ov win win-bottom) (if (overlay-get ov 'kitty-gfx-last-row) (cl-incf placed) (cl-incf hidden))))) @@ -1508,15 +1525,16 @@ to prevent flicker." (kitty-gfx--log "refresh: done total=%d placed=%d hidden=%d pruned=%d" total-overlays placed hidden pruned)))) -(defun kitty-gfx--refresh-overlay (ov win-bottom) - "Refresh a single overlay OV. WIN-BOTTOM is the window's bottom edge. -Dispatches to heading or image refresh based on overlay type." +(defun kitty-gfx--refresh-overlay (ov win win-bottom) + "Refresh a single overlay OV in WIN. +WIN-BOTTOM is WIN's bottom edge. Dispatches to heading or image +refresh based on overlay type." (if (overlay-get ov 'kitty-gfx-heading) ;; Heading overlay — phase 1: compute position + erase if moved. ;; OSC 66 emission happens in phase 2 (kitty-gfx--emit-heading-overlays). - (kitty-gfx--refresh-heading-overlay ov win-bottom) + (kitty-gfx--refresh-heading-overlay ov win win-bottom) ;; Image overlay refresh - (let ((pos (kitty-gfx--overlay-screen-pos ov)) + (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))) @@ -1552,14 +1570,15 @@ Dispatches to heading or image refresh based on overlay type." (overlay-put ov 'kitty-gfx-last-col nil) (funcall (kitty-gfx--backend-fn 'delete) ov id pid))))))) -(defun kitty-gfx--refresh-heading-overlay (ov win-bottom) - "Refresh heading overlay OV. WIN-BOTTOM is the window's bottom edge. -Phase 1 of two-phase heading refresh: computes screen position, -erases old multicell block if heading moved or became hidden, -and caches the new position. Does NOT emit OSC 66 — that -happens in phase 2 (`kitty-gfx--emit-heading-overlays') to -avoid posn-at-point redraws destroying freshly-placed blocks." - (let ((pos (kitty-gfx--overlay-screen-pos ov)) +(defun kitty-gfx--refresh-heading-overlay (ov win win-bottom) + "Refresh heading overlay OV in WIN. +WIN-BOTTOM is WIN's bottom edge. Phase 1 of two-phase heading +refresh: computes screen position, erases old multicell block if +heading moved or became hidden, and caches the new position. Does +NOT emit OSC 66 — that happens in phase 2 +(`kitty-gfx--emit-heading-overlays') to avoid posn-at-point redraws +destroying freshly-placed blocks." + (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))) @@ -1995,9 +2014,10 @@ The buffer should be writable (caller handles `inhibit-read-only')." (unless cached-id (when (funcall (kitty-gfx--backend-fn 'prepare) abs-file image-id) (kitty-gfx--cache-put abs-file image-id))) - ;; Create overlay at the scaled dimensions + ;; Create overlay at the scaled dimensions. (when (or cached-id (gethash abs-file kitty-gfx--image-cache)) - (kitty-gfx--make-overlay img-start (point) image-id img-cols img-rows abs-file reuse-pid) + (kitty-gfx--make-overlay img-start (point) image-id + img-cols img-rows abs-file reuse-pid) (kitty-gfx--schedule-refresh))))) (defun kitty-gfx-remove-images (&optional beg end) -- cgit v1.3.1 From 10cd1e5645bbc01f1bfcdb252e1a0a03693c229c Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 29 Apr 2026 11:50:17 +0200 Subject: Handle doc-view image navigation in terminals When kitty-graphics renders doc-view pages in a terminal, doc-view still calls image-mode navigation helpers for scrolling and edge movement. Those helpers expect an Emacs image display and can error or do nothing in the terminal renderer. Add terminal-only doc-view advice for the image navigation functions so they update window hscroll/vscroll and schedule a graphics refresh instead. --- kitty-graphics.el | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/kitty-graphics.el b/kitty-graphics.el index 2137867..a86d08d 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -2801,6 +2801,160 @@ Values > 1.0 zoom in, < 1.0 zoom out.") "Path to the current doc-view page image file. Stored so zoom commands can re-render without querying `doc-view-current-image'.") +(defun kitty-gfx--doc-view-terminal-p () + "Non-nil when Kitty graphics is handling a doc-view buffer in a terminal." + (and kitty-graphics-mode + (not (display-graphic-p)) + (eq major-mode 'doc-view-mode))) + +(defun kitty-gfx--doc-view-image-cell-size () + "Return the current doc-view Kitty image size as (COLS . ROWS), or nil." + (when (overlayp kitty-gfx--doc-view-overlay) + (let ((cols (overlay-get kitty-gfx--doc-view-overlay 'kitty-gfx-cols)) + (rows (overlay-get kitty-gfx--doc-view-overlay 'kitty-gfx-rows))) + (when (and cols rows) + (cons cols rows))))) + +(defun kitty-gfx--doc-view-max-hscroll () + "Return the maximum horizontal scroll for the current Kitty doc-view page." + (let ((size (kitty-gfx--doc-view-image-cell-size))) + (max 0 (- (or (car size) 0) (window-body-width))))) + +(defun kitty-gfx--doc-view-max-vscroll () + "Return the maximum vertical pixel scroll for the current Kitty doc-view page." + (let ((size (kitty-gfx--doc-view-image-cell-size))) + (max 0 (- (* (or (cdr size) 0) (frame-char-height)) + (window-body-height nil t))))) + +(defun kitty-gfx--doc-view-set-hscroll (ncols) + "Set horizontal scroll to NCOLS for Kitty doc-view and refresh the page." + (let ((new (max 0 (min ncols (kitty-gfx--doc-view-max-hscroll))))) + (set-window-hscroll (selected-window) new) + (kitty-gfx--schedule-refresh) + new)) + +(defun kitty-gfx--doc-view-set-vscroll (pixels) + "Set vertical pixel scroll to PIXELS for Kitty doc-view and refresh the page." + (let ((new (max 0 (min pixels (kitty-gfx--doc-view-max-vscroll))))) + (set-window-vscroll (selected-window) new t) + (kitty-gfx--schedule-refresh) + new)) + +(defun kitty-gfx--doc-view-forward-hscroll (&optional n) + "Scroll the current Kitty doc-view page left by N columns." + (kitty-gfx--doc-view-set-hscroll (+ (window-hscroll) (or n 1)))) + +(defun kitty-gfx--doc-view-next-line (&optional n) + "Scroll the current Kitty doc-view page upward by N terminal rows." + (kitty-gfx--doc-view-set-vscroll + (+ (window-vscroll nil t) (* (or n 1) (frame-char-height))))) + +(defun kitty-gfx--doc-view-scroll-left (&optional n) + "Scroll the current Kitty doc-view page leftward by N columns." + (kitty-gfx--doc-view-forward-hscroll + (cond + ((null n) (max 0 (- (window-body-width) 2))) + ((eq n '-) (min 0 (- 2 (window-body-width)))) + (t (prefix-numeric-value n))))) + +(defun kitty-gfx--doc-view-scroll-up (&optional n) + "Scroll the current Kitty doc-view page upward by N rows." + (kitty-gfx--doc-view-next-line + (cond + ((null n) (max 0 (- (window-body-height) next-screen-context-lines))) + ((eq n '-) (min 0 (- next-screen-context-lines (window-body-height)))) + (t (prefix-numeric-value n))))) + +(defun kitty-gfx--doc-view-image-forward-hscroll-advice (orig-fn &optional n) + "Around advice for `image-forward-hscroll' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (kitty-gfx--doc-view-forward-hscroll n) + (funcall orig-fn n))) + +(defun kitty-gfx--doc-view-image-backward-hscroll-advice (orig-fn &optional n) + "Around advice for `image-backward-hscroll' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (kitty-gfx--doc-view-forward-hscroll (- (or n 1))) + (funcall orig-fn n))) + +(defun kitty-gfx--doc-view-image-next-line-advice (orig-fn &optional n) + "Around advice for `image-next-line' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (kitty-gfx--doc-view-next-line n) + (funcall orig-fn n))) + +(defun kitty-gfx--doc-view-image-previous-line-advice (orig-fn &optional n) + "Around advice for `image-previous-line' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (kitty-gfx--doc-view-next-line (- (or n 1))) + (funcall orig-fn n))) + +(defun kitty-gfx--doc-view-image-scroll-left-advice (orig-fn &optional n) + "Around advice for `image-scroll-left' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (kitty-gfx--doc-view-scroll-left n) + (funcall orig-fn n))) + +(defun kitty-gfx--doc-view-image-scroll-right-advice (orig-fn &optional n) + "Around advice for `image-scroll-right' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (kitty-gfx--doc-view-scroll-left + (cond + ((null n) (min 0 (- 2 (window-body-width)))) + ((eq n '-) (max 0 (- (window-body-width) 2))) + (t (- (prefix-numeric-value n))))) + (funcall orig-fn n))) + +(defun kitty-gfx--doc-view-image-scroll-up-advice (orig-fn &optional n) + "Around advice for `image-scroll-up' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (kitty-gfx--doc-view-scroll-up n) + (funcall orig-fn n))) + +(defun kitty-gfx--doc-view-image-scroll-down-advice (orig-fn &optional n) + "Around advice for `image-scroll-down' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (kitty-gfx--doc-view-scroll-up + (cond + ((null n) (min 0 (- next-screen-context-lines (window-body-height)))) + ((eq n '-) (max 0 (- (window-body-height) next-screen-context-lines))) + (t (- (prefix-numeric-value n))))) + (funcall orig-fn n))) + +(defun kitty-gfx--doc-view-image-bol-advice (orig-fn &optional arg) + "Around advice for `image-bol' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (progn + (when (and arg (/= (prefix-numeric-value arg) 1)) + (kitty-gfx--doc-view-next-line (- (prefix-numeric-value arg) 1))) + (kitty-gfx--doc-view-set-hscroll 0)) + (funcall orig-fn arg))) + +(defun kitty-gfx--doc-view-image-eol-advice (orig-fn &optional arg) + "Around advice for `image-eol' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (progn + (when (and arg (/= (prefix-numeric-value arg) 1)) + (kitty-gfx--doc-view-next-line (- (prefix-numeric-value arg) 1))) + (kitty-gfx--doc-view-set-hscroll (kitty-gfx--doc-view-max-hscroll))) + (funcall orig-fn arg))) + +(defun kitty-gfx--doc-view-image-bob-advice (orig-fn) + "Around advice for `image-bob' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (progn + (kitty-gfx--doc-view-set-hscroll 0) + (kitty-gfx--doc-view-set-vscroll 0)) + (funcall orig-fn))) + +(defun kitty-gfx--doc-view-image-eob-advice (orig-fn) + "Around advice for `image-eob' in Kitty doc-view buffers." + (if (kitty-gfx--doc-view-terminal-p) + (progn + (kitty-gfx--doc-view-set-hscroll (kitty-gfx--doc-view-max-hscroll)) + (kitty-gfx--doc-view-set-vscroll (kitty-gfx--doc-view-max-vscroll))) + (funcall orig-fn))) + (defun kitty-gfx--doc-view-insert-image-advice (orig-fn file &rest args) "Around advice for `doc-view-insert-image'. Displays the page image via Kitty graphics instead of an Emacs @@ -2822,6 +2976,8 @@ image spec. FILE is the path to the page PNG." (win-w (- (window-body-width) 1)) (win-h (- (window-body-height) 1))) (erase-buffer) + (set-window-hscroll (selected-window) 0) + (set-window-vscroll (selected-window) 0 t) (kitty-gfx--display-image-centered file win-w win-h win-w win-h kitty-gfx--doc-view-scale old-pid) @@ -3256,7 +3412,31 @@ Requires `kitty-gfx-enable-video' to be non-nil and mpv installed." (advice-add 'doc-view-enlarge :around #'kitty-gfx--doc-view-enlarge-advice) (advice-add 'doc-view-scale-reset :around - #'kitty-gfx--doc-view-scale-reset-advice)) + #'kitty-gfx--doc-view-scale-reset-advice) + (advice-add 'image-forward-hscroll :around + #'kitty-gfx--doc-view-image-forward-hscroll-advice) + (advice-add 'image-backward-hscroll :around + #'kitty-gfx--doc-view-image-backward-hscroll-advice) + (advice-add 'image-next-line :around + #'kitty-gfx--doc-view-image-next-line-advice) + (advice-add 'image-previous-line :around + #'kitty-gfx--doc-view-image-previous-line-advice) + (advice-add 'image-scroll-left :around + #'kitty-gfx--doc-view-image-scroll-left-advice) + (advice-add 'image-scroll-right :around + #'kitty-gfx--doc-view-image-scroll-right-advice) + (advice-add 'image-scroll-up :around + #'kitty-gfx--doc-view-image-scroll-up-advice) + (advice-add 'image-scroll-down :around + #'kitty-gfx--doc-view-image-scroll-down-advice) + (advice-add 'image-bol :around + #'kitty-gfx--doc-view-image-bol-advice) + (advice-add 'image-eol :around + #'kitty-gfx--doc-view-image-eol-advice) + (advice-add 'image-bob :around + #'kitty-gfx--doc-view-image-bob-advice) + (advice-add 'image-eob :around + #'kitty-gfx--doc-view-image-eob-advice)) (with-eval-after-load 'markdown-overlays (advice-add 'markdown-overlays--fontify-image :around #'kitty-gfx--markdown-overlays-fontify-image-advice) @@ -3281,6 +3461,18 @@ Requires `kitty-gfx-enable-video' to be non-nil and mpv installed." (advice-remove 'doc-view-insert-image #'kitty-gfx--doc-view-insert-image-advice) (advice-remove 'doc-view-enlarge #'kitty-gfx--doc-view-enlarge-advice) (advice-remove 'doc-view-scale-reset #'kitty-gfx--doc-view-scale-reset-advice) + (advice-remove 'image-forward-hscroll #'kitty-gfx--doc-view-image-forward-hscroll-advice) + (advice-remove 'image-backward-hscroll #'kitty-gfx--doc-view-image-backward-hscroll-advice) + (advice-remove 'image-next-line #'kitty-gfx--doc-view-image-next-line-advice) + (advice-remove 'image-previous-line #'kitty-gfx--doc-view-image-previous-line-advice) + (advice-remove 'image-scroll-left #'kitty-gfx--doc-view-image-scroll-left-advice) + (advice-remove 'image-scroll-right #'kitty-gfx--doc-view-image-scroll-right-advice) + (advice-remove 'image-scroll-up #'kitty-gfx--doc-view-image-scroll-up-advice) + (advice-remove 'image-scroll-down #'kitty-gfx--doc-view-image-scroll-down-advice) + (advice-remove 'image-bol #'kitty-gfx--doc-view-image-bol-advice) + (advice-remove 'image-eol #'kitty-gfx--doc-view-image-eol-advice) + (advice-remove 'image-bob #'kitty-gfx--doc-view-image-bob-advice) + (advice-remove 'image-eob #'kitty-gfx--doc-view-image-eob-advice) (advice-remove 'image-mode #'kitty-gfx--image-mode-advice) (advice-remove 'shr-put-image #'kitty-gfx--shr-put-image-advice) (advice-remove 'markdown-overlays--fontify-image #'kitty-gfx--markdown-overlays-fontify-image-advice) -- cgit v1.3.1 From 064ef03881008e93699d8f3343fb19fadada68ee Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 29 Apr 2026 11:30:59 +0200 Subject: Render doc-view pages without mutating PDF buffers Terminal doc-view rendering used to erase and insert padding/newlines into the doc-view buffer before placing the page image. doc-view buffers visit the original PDF, so changing buffer text marks the document modified and can corrupt it if saved. Create the Kitty/Sixel page overlay over the existing buffer contents instead. Compute the page image dimensions, prepare/cache the page image, and schedule a refresh without changing the visited PDF bytes. --- kitty-graphics.el | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/kitty-graphics.el b/kitty-graphics.el index a86d08d..0bed240 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -2971,17 +2971,34 @@ image spec. FILE is the path to the page PNG." (when kitty-gfx--doc-view-overlay (kitty-gfx--remove-overlay kitty-gfx--doc-view-overlay old-pid) (setq kitty-gfx--doc-view-overlay nil)) - ;; Clear the buffer (removes "Welcome to DocView!" text) - (let* ((inhibit-read-only t) - (win-w (- (window-body-width) 1)) - (win-h (- (window-body-height) 1))) - (erase-buffer) + ;; Display the rendered page using only an overlay. Do not erase + ;; or insert text here: doc-view buffers visit the original PDF, so + ;; mutating buffer text can corrupt the document if it is saved. + (let* ((win-w (- (window-body-width) 1)) + (win-h (- (window-body-height) 1)) + (abs-file (expand-file-name file)) + (px (kitty-gfx--image-pixel-size abs-file)) + (base-dims (if px + (kitty-gfx--compute-cell-dims + (car px) (cdr px) win-w win-h) + (cons (min 40 win-w) (min 15 win-h)))) + (img-cols (max 1 (round (* kitty-gfx--doc-view-scale + (car base-dims))))) + (img-rows (max 1 (round (* kitty-gfx--doc-view-scale + (cdr base-dims))))) + (cached-id (kitty-gfx--cache-get abs-file)) + (image-id (or cached-id (kitty-gfx--alloc-id)))) (set-window-hscroll (selected-window) 0) (set-window-vscroll (selected-window) 0 t) - (kitty-gfx--display-image-centered - file win-w win-h win-w win-h - kitty-gfx--doc-view-scale old-pid) - (setq kitty-gfx--doc-view-overlay (car kitty-gfx--overlays)))) + (unless cached-id + (when (funcall (kitty-gfx--backend-fn 'prepare) abs-file image-id) + (kitty-gfx--cache-put abs-file image-id))) + (when (or cached-id (gethash abs-file kitty-gfx--image-cache)) + (setq kitty-gfx--doc-view-overlay + (kitty-gfx--make-overlay (point-min) (point-max) + image-id img-cols img-rows + abs-file old-pid)) + (kitty-gfx--schedule-refresh)))) (goto-char (point-min))) (apply orig-fn file args))) -- cgit v1.3.1 From e68696f0baae66cd17d4a7f0f3d021ca906f49b3 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 29 Apr 2026 11:52:26 +0200 Subject: Delete image placements before window rerenders Window layout changes move image overlays before kitty-graphics recalculates terminal positions. Clearing last-row/last-col first loses the only geometry the backends can use to delete the old terminal placement, which leaves stale pixels over new split separators. Delete the current recorded image placement during window-change handling before clearing the cached position. Also make image-mode placement reuse explicit: zoom commands still reuse a PID to avoid flicker, while split/resize rerenders delete first. --- kitty-graphics.el | 80 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/kitty-graphics.el b/kitty-graphics.el index 0bed240..210d52b 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -1694,27 +1694,45 @@ then invalidates position caches and schedules a refresh." (defun kitty-gfx--on-window-change (_frame) "Handle window configuration change for image refresh. -Invalidates position caches and cell pixel size so the refresh -cycle re-places images correctly. Does NOT delete all placements -— that causes visible flicker. Uses a longer debounce than normal -refresh to let Emacs finish window layout transitions (e.g., when -closing a split, Emacs briefly shows two windows for the same -buffer before settling to one)." - (kitty-gfx--log "on-window-change: invalidating positions and cell size") +Invalidates cell pixel size, deletes stale image placements, then +clears image position caches so the refresh cycle re-places images +at their new positions. Uses a longer debounce than normal refresh +to let Emacs finish window layout transitions (e.g., when closing a +split, Emacs briefly shows two windows for the same buffer before +settling to one)." + (kitty-gfx--log "on-window-change: deleting stale placements and invalidating cell size") (setq kitty-gfx--cell-pixel-width nil kitty-gfx--cell-pixel-height nil) - ;; Reset position cache so images get re-placed at correct positions. + ;; 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. + ;; ;; Heading overlays PRESERVE their cache — the refresh cycle needs ;; old→new position comparison to erase multicell blocks properly. - ;; Images can clear cache because re-placing with the same PID is - ;; idempotent (the terminal replaces the old placement). - (dolist (buf (buffer-list)) - (with-current-buffer buf - (dolist (ov kitty-gfx--overlays) - (when (and (overlay-buffer ov) - (not (overlay-get ov 'kitty-gfx-heading))) - (overlay-put ov 'kitty-gfx-last-row nil) - (overlay-put ov 'kitty-gfx-last-col nil))))) + (kitty-gfx--sync-begin) + (unwind-protect + (dolist (buf (buffer-list)) + (with-current-buffer buf + (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--sync-end)) ;; Longer debounce: cancel any fast leading-edge cooldown and ;; schedule a 0.1s delayed refresh to let window layout settle. (when kitty-gfx--render-timer @@ -2647,8 +2665,13 @@ With an active region, restrict to it; otherwise clear the whole buffer." "Zoom scale factor for image-mode display. Values > 1.0 zoom in, < 1.0 zoom out.") -(defun kitty-gfx--image-mode-render () - "Render the current image file centered at current scale." +(defun kitty-gfx--image-mode-render (&optional reuse-placement) + "Render the current image file centered at current scale. +When REUSE-PLACEMENT is non-nil, reuse the old terminal placement +ID instead of deleting it first. This is useful for zoom commands +where the new placement immediately replaces the old one, but it is +intentionally not used for window size changes because stale pixels +can otherwise remain over newly-created window separators." (when-let* ((file (buffer-file-name))) (when (kitty-gfx--image-file-p file) (let* ((inhibit-read-only t) @@ -2656,15 +2679,18 @@ Values > 1.0 zoom in, < 1.0 zoom out.") (win-h (- (window-body-height) 2)) (max-cols (min win-w kitty-gfx-max-width)) (max-rows (min win-h kitty-gfx-max-height)) - ;; Save the old placement ID before removing overlays. - ;; Reusing it avoids delete+re-place glitches (WezTerm #5892). - (old-pid (when (car kitty-gfx--overlays) + ;; Save the old placement ID only when the caller explicitly + ;; wants to reuse it. Reusing avoids delete+re-place glitches + ;; for zoom commands (WezTerm #5892), but window splits/resizes + ;; must delete first so stale terminal pixels are cleared. + (old-pid (when (and reuse-placement (car kitty-gfx--overlays)) (overlay-get (car kitty-gfx--overlays) 'kitty-gfx-pid)))) (kitty-gfx--log "image-mode-render: file=%s scale=%.2f win=%dx%d max=%dx%d reuse-pid=%s" (file-name-nondirectory file) kitty-gfx--image-scale win-w win-h max-cols max-rows old-pid) - ;; Remove overlays but skip terminal-side delete when we have - ;; a PID to reuse (the new placement will atomically replace it). + ;; Remove overlays. When OLD-PID is non-nil, skip terminal-side + ;; delete so the new placement atomically replaces it; otherwise + ;; delete/erase the old placement before changing the buffer text. (dolist (ov (overlays-in (point-min) (point-max))) (when (overlay-get ov 'kitty-gfx) (kitty-gfx--remove-overlay ov old-pid))) @@ -2679,19 +2705,19 @@ Values > 1.0 zoom in, < 1.0 zoom out.") "Zoom in on the image in image-mode." (interactive) (setq kitty-gfx--image-scale (* kitty-gfx--image-scale 1.25)) - (kitty-gfx--image-mode-render)) + (kitty-gfx--image-mode-render t)) (defun kitty-gfx-image-decrease-size () "Zoom out on the image in image-mode." (interactive) (setq kitty-gfx--image-scale (max 0.1 (* kitty-gfx--image-scale 0.8))) - (kitty-gfx--image-mode-render)) + (kitty-gfx--image-mode-render t)) (defun kitty-gfx-image-reset-size () "Reset image zoom to default in image-mode." (interactive) (setq kitty-gfx--image-scale 1.0) - (kitty-gfx--image-mode-render)) + (kitty-gfx--image-mode-render t)) (defun kitty-gfx--image-mode-advice (orig-fn &rest args) "Around advice for `image-mode'." -- cgit v1.3.1 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