From e8f259d8c03126d548ec28dd9a4bcc41096ab9f9 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 27 Feb 2026 08:29:34 +0100 Subject: feat: robustness overhaul — LRU cache, better error handling, reduced flicker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LRU image cache with configurable size and eviction (kitty-gfx-cache-size) - Cache stores only image IDs; dimensions computed fresh each time - Leading-edge debounce for faster initial refresh response - Targeted placement deletion on buffer/window change (no more delete-all flicker) - Cell pixel size re-queried on window resize - Image overflow check prevents visual corruption at window bottom - convert-to-png returns nil on failure instead of passing non-PNG data - unwind-protect for temp file cleanup in display-image paths - kill-buffer-hook checks cross-buffer image sharing before deleting - ignore-errors around all send-string-to-terminal calls - Millisecond-precision debug logging throughout - Dead overlay pruning in refresh cycle - GIF removed from supported formats (multi-frame issues) - Window-size-change hook in image-mode for split/unsplit re-render --- kitty-graphics.el | 696 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 494 insertions(+), 202 deletions(-) diff --git a/kitty-graphics.el b/kitty-graphics.el index 59b25c3..52f2d60 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -3,7 +3,7 @@ ;; Copyright (C) 2025-2026 ;; ;; Author: cashmere -;; Version: 0.2.0 +;; Version: 0.2.1 ;; URL: https://github.com/cashmeredev/kitty-graphics.el ;; Keywords: terminals, images, multimedia ;; Package-Requires: ((emacs "27.1")) @@ -105,8 +105,9 @@ This debounces rapid redisplay events. Default is ~1 frame at 60fps." (defun kitty-gfx--log (fmt &rest args) "Log to `kitty-gfx--log-file' when `kitty-gfx-debug' is non-nil." (when kitty-gfx-debug - (let ((msg (concat (format-time-string "%H:%M:%S ") (apply #'format fmt args) "\n"))) - (append-to-file msg nil kitty-gfx--log-file)))) + (let ((msg (concat (format-time-string "%H:%M:%S.%3N ") + (apply #'format fmt args) "\n"))) + (ignore-errors (append-to-file msg nil kitty-gfx--log-file))))) ;;;; Constants — kept for reference if switching back to Unicode placeholders ;; (defconst kitty-gfx--placeholder-char #x10EEEE) @@ -121,8 +122,22 @@ This debounces rapid redisplay events. Default is ~1 frame at 60fps." "Next image ID to assign (1-4294967295). With direct placements, any uint32 ID works — no 256-color constraint.") +(defcustom kitty-gfx-cache-size 64 + "Maximum number of images to keep in the terminal-side cache. +When exceeded, the least recently used image is evicted and its +terminal data deleted via `a=d'." + :type 'integer + :group 'kitty-graphics) + (defvar kitty-gfx--image-cache (make-hash-table :test 'equal) - "Maps file paths to (image-id . (cols . rows)).") + "Maps file paths to image IDs (integers). +Only stores the terminal-side image ID — display dimensions are +computed fresh each time to avoid stale values from different +display contexts (window sizes, zoom levels, etc.).") + +(defvar kitty-gfx--cache-lru nil + "LRU list of file paths in `kitty-gfx--image-cache'. +Most recently used at the front.") (defvar-local kitty-gfx--overlays nil "Image overlays in this buffer.") @@ -147,11 +162,15 @@ rather than accumulate.") (defun kitty-gfx--supported-p () "Return non-nil if the terminal supports Kitty graphics." - (and (not (display-graphic-p)) - (or (getenv "KITTY_PID") - (equal (getenv "TERM_PROGRAM") "kitty") - (equal (getenv "TERM_PROGRAM") "WezTerm") - (equal (getenv "TERM_PROGRAM") "ghostty")))) + (let ((supported (and (not (display-graphic-p)) + (or (getenv "KITTY_PID") + (equal (getenv "TERM_PROGRAM") "kitty") + (equal (getenv "TERM_PROGRAM") "WezTerm") + (equal (getenv "TERM_PROGRAM") "ghostty"))))) + (kitty-gfx--log "supported-p: %s (graphic=%s KITTY_PID=%s TERM_PROGRAM=%s)" + supported (display-graphic-p) + (getenv "KITTY_PID") (getenv "TERM_PROGRAM")) + supported)) (defun kitty-gfx--query-cell-size () "Query terminal for cell size in pixels using CSI 16 t (XTWINOPS). @@ -197,11 +216,13 @@ Falls back to reasonable defaults if query fails or times out." "Begin synchronized output (BSU). The terminal buffers output until `kitty-gfx--sync-end' is called, preventing partial rendering and flicker." + (kitty-gfx--log "sync-begin") (ignore-errors (send-string-to-terminal "\e[?2026h"))) (defun kitty-gfx--sync-end () "End synchronized output (ESU). Flushes buffered output to the terminal all at once." + (kitty-gfx--log "sync-end") (ignore-errors (send-string-to-terminal "\e[?2026l"))) ;;;; Protocol layer @@ -214,7 +235,10 @@ with `kitty-gfx--place-image'." (let* ((chunk-size kitty-gfx-chunk-size) (len (length base64-data)) (offset 0) - (first t)) + (first t) + (chunk-count 0)) + (kitty-gfx--log "transmit-begin: id=%d b64-len=%d chunk-size=%d chunks=%d" + id len chunk-size (ceiling (/ (float len) chunk-size))) (while (< offset len) (let* ((end (min (+ offset chunk-size) len)) (chunk (substring base64-data offset end)) @@ -222,18 +246,22 @@ with `kitty-gfx--place-image'." (ctrl (if first (format "a=t,q=2,f=100,i=%d,m=%d" id more) (format "m=%d,q=2" more)))) - (send-string-to-terminal (format "\e_G%s;%s\e\\" ctrl chunk)) + (ignore-errors + (send-string-to-terminal (format "\e_G%s;%s\e\\" ctrl chunk))) + (cl-incf chunk-count) (setq offset end first nil))) - (kitty-gfx--log "transmitted image: id=%d b64-len=%d" id len))) + (kitty-gfx--log "transmit-done: id=%d chunks-sent=%d" id chunk-count))) (defun kitty-gfx--delete-by-id (id) "Delete image with ID and free data." + (kitty-gfx--log "delete-by-id: id=%d" id) (ignore-errors (send-string-to-terminal (format "\e_Ga=d,d=I,i=%d,q=2\e\\" id)))) (defun kitty-gfx--delete-all-images () "Delete all visible placements and free data." + (kitty-gfx--log "delete-all-images") (ignore-errors (send-string-to-terminal "\e_Ga=d,d=A,q=2\e\\"))) @@ -245,6 +273,7 @@ with `kitty-gfx--place-image'." (setq kitty-gfx--next-placement-id (1+ kitty-gfx--next-placement-id)) (when (> kitty-gfx--next-placement-id 4294967295) (setq kitty-gfx--next-placement-id 1)) + (kitty-gfx--log "alloc-pid: %d" pid) pid)) (defun kitty-gfx--place-image (image-id placement-id cols rows term-row term-col) @@ -255,9 +284,10 @@ COLS x ROWS is the size in terminal cells. Uses direct placement: move cursor, then `a=p' with `c' and `r' params." (kitty-gfx--log "place: id=%d pid=%d cols=%d rows=%d row=%d col=%d" image-id placement-id cols rows term-row term-col) - (send-string-to-terminal - (format "\e7\e[%d;%dH\e_Gq=2,a=p,i=%d,p=%d,c=%d,r=%d\e\\\e8" - term-row term-col image-id placement-id cols rows))) + (ignore-errors + (send-string-to-terminal + (format "\e7\e[%d;%dH\e_Gq=2,a=p,i=%d,p=%d,c=%d,r=%d\e\\\e8" + term-row term-col image-id placement-id cols rows)))) ;;;; Position mapping @@ -266,17 +296,19 @@ Uses direct placement: move cursor, then `a=p' with `c' and `r' params." Checks org-fold (org 9.6+, text-property based) first, then falls back to overlay-based invisibility for legacy org and outline-mode. Ignores cosmetic invisibility like hidden link brackets (`org-link')." - (or - ;; org-fold (org 9.6+): text-property based folding. - ;; org-fold-folded-p only checks structural folds (outline, block, - ;; drawer), not cosmetic link bracket hiding. - (and (fboundp 'org-fold-folded-p) - (condition-case nil - (org-fold-folded-p pos) - (error nil))) - ;; Legacy / non-org overlay-based folding (outline-mode, etc.) - (let ((inv (get-char-property pos 'invisible))) - (and inv (not (eq inv 'org-link)))))) + (let ((folded + (or + ;; org-fold (org 9.6+): text-property based folding. + (and (fboundp 'org-fold-folded-p) + (condition-case nil + (org-fold-folded-p pos) + (error nil))) + ;; Legacy / non-org overlay-based folding (outline-mode, etc.) + (let ((inv (get-char-property pos 'invisible))) + (and inv (not (eq inv 'org-link))))))) + (when folded + (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. @@ -287,6 +319,14 @@ the window's scroll range." (let* ((buf (overlay-buffer ov)) (pos (overlay-start ov)) (win (and buf (get-buffer-window buf)))) + (unless (and win pos + (pos-visible-in-window-p pos win) + (not (kitty-gfx--in-folded-region-p pos))) + (kitty-gfx--log "screen-pos: pid=%s pos=%s HIDDEN (win=%s visible=%s folded=%s)" + (overlay-get ov 'kitty-gfx-pid) pos + (and win t) + (and win pos (pos-visible-in-window-p pos win)) + (and pos (kitty-gfx--in-folded-region-p pos)))) (when (and win pos (pos-visible-in-window-p pos win) ;; Check that the text isn't hidden by structural folding @@ -317,8 +357,11 @@ the window's scroll range." (+ body-left buf-col) (+ win-left posn-col)))) (when col-row - (cons (+ win-top row 1) - (1+ col))))))))) + (let ((result (cons (+ win-top row 1) (1+ col)))) + (kitty-gfx--log "screen-pos: pid=%s pos=%d -> row=%d col=%d" + (overlay-get ov 'kitty-gfx-pid) pos + (car result) (cdr result)) + result)))))))) ;;;; Refresh cycle @@ -331,19 +374,43 @@ Deletes placements for overlays that scrolled out of view. All terminal output is wrapped in synchronized output (BSU/ESU) to prevent flicker." (when (and kitty-graphics-mode (not (display-graphic-p))) - (kitty-gfx--sync-begin) - (unwind-protect - (walk-windows - (lambda (win) - (with-current-buffer (window-buffer win) - (when kitty-gfx--overlays - (let* ((edges (window-edges win)) - (win-bottom (nth 3 edges))) - (dolist (ov kitty-gfx--overlays) - (when (overlay-buffer ov) - (kitty-gfx--refresh-overlay ov win-bottom))))))) - nil 'visible) - (kitty-gfx--sync-end)))) + ;; Re-query cell size if invalidated (e.g., after terminal resize) + (unless (and kitty-gfx--cell-pixel-width kitty-gfx--cell-pixel-height) + (kitty-gfx--query-cell-size)) + (let ((total-overlays 0) + (placed 0) + (hidden 0) + (pruned 0)) + (kitty-gfx--log "refresh: begin") + (kitty-gfx--sync-begin) + (unwind-protect + (walk-windows + (lambda (win) + (with-current-buffer (window-buffer win) + (when kitty-gfx--overlays + ;; Prune dead overlays (overlay-buffer returns nil) + (let ((before (length kitty-gfx--overlays))) + (setq kitty-gfx--overlays + (cl-delete-if-not #'overlay-buffer kitty-gfx--overlays)) + (let ((removed (- before (length kitty-gfx--overlays)))) + (when (> removed 0) + (cl-incf pruned removed) + (kitty-gfx--log "refresh: pruned %d dead overlays from %s" + removed (buffer-name))))) + (let* ((edges (window-edges win)) + (win-bottom (nth 3 edges))) + (kitty-gfx--log "refresh: win=%s buf=%s overlays=%d bottom=%d" + 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) + (if (overlay-get ov 'kitty-gfx-last-row) + (cl-incf placed) + (cl-incf hidden))))))) + nil 'visible) + (kitty-gfx--sync-end)) + (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. @@ -353,77 +420,140 @@ if the overlay scrolled out of view." (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))) - (if (and pos (<= (car pos) win-bottom)) - ;; Visible (start is on screen) — place if position changed - (let ((new-row (car pos)) - (new-col (cdr pos))) - (unless (and (eql new-row last-row) - (eql new-col last-col)) - (overlay-put ov 'kitty-gfx-last-row new-row) - (overlay-put ov 'kitty-gfx-last-col new-col) - (kitty-gfx--place-image - (overlay-get ov 'kitty-gfx-id) - (overlay-get ov 'kitty-gfx-pid) - (overlay-get ov 'kitty-gfx-cols) - rows new-row new-col))) - ;; Not visible or overflows — delete if was placed - (when last-row - (overlay-put ov 'kitty-gfx-last-row nil) - (overlay-put ov 'kitty-gfx-last-col nil) - (kitty-gfx--delete-placement - (overlay-get ov 'kitty-gfx-id) - (overlay-get ov 'kitty-gfx-pid)))))) + (let ((pid (overlay-get ov 'kitty-gfx-pid)) + (id (overlay-get ov 'kitty-gfx-id))) + (if (and pos + ;; Start row is on screen + (<= (car pos) win-bottom) + ;; Image must fit: start + rows must not exceed win-bottom. + ;; This prevents placing images that overflow the terminal + ;; and cause visual corruption. + (<= (+ (car pos) rows -1) win-bottom)) + ;; Visible and fits — place if position changed + (let ((new-row (car pos)) + (new-col (cdr pos))) + (if (and (eql new-row last-row) + (eql new-col last-col)) + (kitty-gfx--log "refresh-ov: pid=%d unchanged at row=%d col=%d" + pid new-row new-col) + (kitty-gfx--log "refresh-ov: pid=%d moved %s -> row=%d col=%d" + pid + (if last-row (format "row=%d,col=%d" last-row last-col) "nil") + new-row new-col) + (overlay-put ov 'kitty-gfx-last-row new-row) + (overlay-put ov 'kitty-gfx-last-col new-col) + (kitty-gfx--place-image 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) + (overlay-put ov 'kitty-gfx-last-row nil) + (overlay-put ov 'kitty-gfx-last-col nil) + (kitty-gfx--delete-placement id pid)))))) + +(defvar kitty-gfx--refresh-pending nil + "Non-nil if a refresh was requested during the cooldown period.") (defun kitty-gfx--schedule-refresh () - "Schedule an image refresh after the current redisplay completes." - (when kitty-gfx--render-timer - (cancel-timer kitty-gfx--render-timer)) - (setq kitty-gfx--render-timer - (run-at-time kitty-gfx-render-delay nil - (lambda () - (setq kitty-gfx--render-timer nil) - (kitty-gfx--refresh))))) + "Schedule an image refresh using leading-edge debounce. +On the first call, refresh is scheduled via `run-at-time' 0 (fires +after the current redisplay completes) and a cooldown timer starts +\(duration `kitty-gfx-render-delay'). Calls during cooldown are +suppressed but flagged; when the cooldown expires a single trailing +refresh fires to capture the final state." + (if kitty-gfx--render-timer + ;; Cooldown active — flag that another refresh is wanted. + (setq kitty-gfx--refresh-pending t) + ;; No cooldown — schedule refresh after redisplay + start cooldown. + ;; run-at-time 0 ensures posn-at-point sees up-to-date positions. + (setq kitty-gfx--refresh-pending nil) + (run-at-time 0 nil #'kitty-gfx--refresh) + (setq kitty-gfx--render-timer + (run-at-time kitty-gfx-render-delay nil + (lambda () + (setq kitty-gfx--render-timer nil) + (when kitty-gfx--refresh-pending + (setq kitty-gfx--refresh-pending nil) + (kitty-gfx--refresh))))))) (defun kitty-gfx--on-window-scroll (win _new-start) "Handle window scroll for image refresh." (when (buffer-local-value 'kitty-gfx--overlays (window-buffer win)) + (kitty-gfx--log "on-scroll: win=%s buf=%s" win (buffer-name (window-buffer win))) (kitty-gfx--schedule-refresh))) (defun kitty-gfx--on-buffer-change (_frame-or-window) "Handle buffer change for image refresh. -Clears visible placements first since the displayed buffer changed." - (kitty-gfx--sync-begin) - (unwind-protect - (progn - (ignore-errors - (send-string-to-terminal "\e_Ga=d,d=a,q=2\e\\")) - ;; Reset position cache so images get re-placed - (dolist (buf (buffer-list)) - (with-current-buffer buf - (dolist (ov kitty-gfx--overlays) - (when (overlay-buffer ov) - (overlay-put ov 'kitty-gfx-last-row nil) - (overlay-put ov 'kitty-gfx-last-col nil)))))) - (kitty-gfx--sync-end)) - (kitty-gfx--schedule-refresh)) +Deletes placements for buffers no longer visible in any window, +then invalidates position caches and schedules a refresh." + (kitty-gfx--log "on-buffer-change: cleaning up non-visible placements") + ;; Find which buffers are currently visible + (let ((visible-bufs nil)) + (walk-windows (lambda (w) (push (window-buffer w) visible-bufs)) + nil 'visible) + ;; Delete placements for buffers that are no longer in any window + (dolist (buf (buffer-list)) + (with-current-buffer buf + (when (and kitty-gfx--overlays + (not (memq buf visible-bufs))) + (kitty-gfx--log "on-buffer-change: deleting placements for hidden buf=%s" + (buffer-name)) + (dolist (ov kitty-gfx--overlays) + (when (overlay-buffer ov) + (let ((id (overlay-get ov 'kitty-gfx-id)) + (pid (overlay-get ov 'kitty-gfx-pid))) + (when (and id pid (overlay-get ov 'kitty-gfx-last-row)) + (kitty-gfx--delete-placement id pid))) + (overlay-put ov 'kitty-gfx-last-row nil) + (overlay-put ov 'kitty-gfx-last-col nil))))))) + ;; Reset cache for visible buffers so they re-place correctly. + (dolist (buf (buffer-list)) + (with-current-buffer buf + (dolist (ov kitty-gfx--overlays) + (when (overlay-buffer ov) + (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 + ;; schedule a 0.1s delayed refresh to let buffer switch settle. + (when kitty-gfx--render-timer + (cancel-timer kitty-gfx--render-timer)) + (setq kitty-gfx--refresh-pending nil + kitty-gfx--render-timer + (run-at-time 0.1 nil + (lambda () + (setq kitty-gfx--render-timer nil) + (kitty-gfx--refresh))))) (defun kitty-gfx--on-window-change (_frame) "Handle window configuration change for image refresh. -Clears visible placements first since window layout changed." - (kitty-gfx--sync-begin) - (unwind-protect - (progn - (ignore-errors - (send-string-to-terminal "\e_Ga=d,d=a,q=2\e\\")) - ;; Reset position cache so images get re-placed - (dolist (buf (buffer-list)) - (with-current-buffer buf - (dolist (ov kitty-gfx--overlays) - (when (overlay-buffer ov) - (overlay-put ov 'kitty-gfx-last-row nil) - (overlay-put ov 'kitty-gfx-last-col nil)))))) - (kitty-gfx--sync-end)) - (kitty-gfx--schedule-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") + (setq kitty-gfx--cell-pixel-width nil + kitty-gfx--cell-pixel-height nil) + ;; Reset position cache so images get re-placed at correct positions. + (dolist (buf (buffer-list)) + (with-current-buffer buf + (dolist (ov kitty-gfx--overlays) + (when (overlay-buffer ov) + (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 + ;; schedule a 0.1s delayed refresh to let window layout settle. + (when kitty-gfx--render-timer + (cancel-timer kitty-gfx--render-timer)) + (setq kitty-gfx--refresh-pending nil + kitty-gfx--render-timer + (run-at-time 0.1 nil + (lambda () + (setq kitty-gfx--render-timer nil) + (kitty-gfx--refresh))))) (defun kitty-gfx--on-redisplay () "Post-command hook to schedule image refresh." @@ -433,11 +563,15 @@ Clears visible placements first since window layout changed." (defun kitty-gfx--read-file-base64 (file) "Read FILE and return base64-encoded string." + (kitty-gfx--log "read-file-base64: %s size=%s" + file (ignore-errors (file-attribute-size (file-attributes file)))) (with-temp-buffer (set-buffer-multibyte nil) (insert-file-contents-literally file) (base64-encode-region (point-min) (point-max) t) - (buffer-string))) + (let ((result (buffer-string))) + (kitty-gfx--log "read-file-base64: done b64-len=%d" (length result)) + result))) (defun kitty-gfx--image-pixel-size (file) "Return (WIDTH . HEIGHT) in pixels for image FILE, or nil." @@ -462,23 +596,38 @@ Clears visible placements first since window layout changed." (defun kitty-gfx--convert-to-png (file) "Convert FILE to PNG if needed. Returns path to PNG file. -Returns FILE unchanged if it is already PNG or if conversion fails." +Returns FILE unchanged if it is already PNG. +Returns nil if FILE is not PNG and ImageMagick is unavailable or +conversion fails — callers must handle nil gracefully." (if (string-suffix-p ".png" file t) - file + (progn + (kitty-gfx--log "convert-to-png: %s already PNG" file) + file) (let ((convert (or (executable-find "magick") (executable-find "convert")))) (if (not convert) - file ; no converter available, try sending as-is + (progn + (kitty-gfx--log "convert-to-png: no ImageMagick, cannot convert %s" file) + (message "kitty-gfx: %s requires ImageMagick for display" + (file-name-nondirectory file)) + nil) (let ((out (make-temp-file "kitty-gfx-" nil ".png"))) - (if (string-suffix-p "magick" convert) - (call-process convert nil nil nil "convert" file out) - (call-process convert nil nil nil file out)) - ;; Check that conversion produced a non-empty file - (if (and (file-exists-p out) - (> (file-attribute-size (file-attributes out)) 0)) - out - (ignore-errors (delete-file out)) - file)))))) + (kitty-gfx--log "convert-to-png: %s -> %s via %s" file out convert) + (let ((exit-code + (if (string-suffix-p "magick" convert) + (call-process convert nil nil nil "convert" file out) + (call-process convert nil nil nil file out)))) + (kitty-gfx--log "convert-to-png: exit-code=%s" exit-code) + ;; Check that conversion produced a non-empty file + (if (and (file-exists-p out) + (> (file-attribute-size (file-attributes out)) 0)) + (progn + (kitty-gfx--log "convert-to-png: success out-size=%d" + (file-attribute-size (file-attributes out))) + out) + (kitty-gfx--log "convert-to-png: FAILED (empty or missing output)") + (ignore-errors (delete-file out)) + nil))))))) (defun kitty-gfx--compute-cell-dims (pixel-w pixel-h max-cols max-rows) "Compute (COLS . ROWS) in terminal cells for image placement. @@ -504,9 +653,54 @@ With direct placements, COLS and ROWS map directly to terminal columns/rows." (let ((id kitty-gfx--next-id)) (setq kitty-gfx--next-id (1+ kitty-gfx--next-id)) (when (> kitty-gfx--next-id 4294967295) + (kitty-gfx--log "alloc-id: WRAP next-id reset to 1") (setq kitty-gfx--next-id 1)) + (kitty-gfx--log "alloc-id: %d" id) id)) +(defun kitty-gfx--cache-touch (file) + "Move FILE to the front of the LRU list (most recently used)." + (setq kitty-gfx--cache-lru + (cons file (delete file kitty-gfx--cache-lru))) + (kitty-gfx--log "cache-touch: %s (lru-len=%d)" (file-name-nondirectory file) + (length kitty-gfx--cache-lru))) + +(defun kitty-gfx--cache-put (file image-id) + "Store IMAGE-ID for FILE in cache, evicting LRU entries if needed." + (kitty-gfx--log "cache-put: %s id=%d (cache-count=%d max=%d)" + (file-name-nondirectory file) image-id + (hash-table-count kitty-gfx--image-cache) kitty-gfx-cache-size) + ;; Evict oldest entries if cache is full + (while (and (> (hash-table-count kitty-gfx--image-cache) + (max 1 kitty-gfx-cache-size)) + kitty-gfx--cache-lru) + (let* ((victim (car (last kitty-gfx--cache-lru))) + (victim-id (gethash victim kitty-gfx--image-cache))) + (when victim-id + (kitty-gfx--delete-by-id victim-id)) + (remhash victim kitty-gfx--image-cache) + (setq kitty-gfx--cache-lru (butlast kitty-gfx--cache-lru)) + (kitty-gfx--log "cache-evict: %s id=%s (remaining=%d)" + (file-name-nondirectory victim) victim-id + (hash-table-count kitty-gfx--image-cache)))) + (puthash file image-id kitty-gfx--image-cache) + (kitty-gfx--cache-touch file)) + +(defun kitty-gfx--cache-get (file) + "Return cached image ID for FILE, or nil. Moves FILE to front of LRU." + (let ((id (gethash file kitty-gfx--image-cache))) + (kitty-gfx--log "cache-get: %s -> %s" (file-name-nondirectory file) + (if id (format "id=%d (hit)" id) "nil (miss)")) + (when id + (kitty-gfx--cache-touch file)) + id)) + +(defun kitty-gfx--cache-remove (file) + "Remove FILE from the cache and LRU list." + (kitty-gfx--log "cache-remove: %s" (file-name-nondirectory file)) + (remhash file kitty-gfx--image-cache) + (setq kitty-gfx--cache-lru (delete file kitty-gfx--cache-lru))) + (defun kitty-gfx--make-blank-display (cols rows) "Create a blank display string of COLS terminal columns x ROWS lines. Each line is propertized with face `default' to prevent org-link @@ -531,27 +725,35 @@ fills with the image via direct placement." ;; Don't set evaporate — zero-width overlays (beg==end) would be ;; deleted immediately if evaporate is set. (push ov kitty-gfx--overlays) + (kitty-gfx--log "make-overlay: id=%d pid=%d cols=%d rows=%d beg=%d end=%d buf=%s (total=%d)" + image-id pid cols rows beg end (buffer-name) (length kitty-gfx--overlays)) ov)) (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 data so the image can be re-placed without retransmitting." + (kitty-gfx--log "delete-placement: id=%d pid=%d" id pid) (ignore-errors (send-string-to-terminal (format "\e_Ga=d,d=i,i=%d,p=%d,q=2\e\\" id pid)))) (defun kitty-gfx--remove-overlay (ov) "Remove overlay OV and delete its placement from terminal." - (when (overlay-buffer ov) - (condition-case nil - (let ((id (overlay-get ov 'kitty-gfx-id)) - (pid (overlay-get ov 'kitty-gfx-pid))) + (let ((id (overlay-get ov 'kitty-gfx-id)) + (pid (overlay-get ov 'kitty-gfx-pid))) + (kitty-gfx--log "remove-overlay: id=%s pid=%s buf=%s" + id pid (when (overlay-buffer ov) (buffer-name (overlay-buffer ov)))) + (when (overlay-buffer ov) + (condition-case err (when (and id pid) - (kitty-gfx--delete-placement id pid))) - (error nil)) - (delete-overlay ov)) - (setq kitty-gfx--overlays (delq ov kitty-gfx--overlays))) + (kitty-gfx--delete-placement id pid)) + (error + (kitty-gfx--log "remove-overlay: error deleting placement: %s" + (error-message-string err)))) + (delete-overlay ov)) + (setq kitty-gfx--overlays (delq ov kitty-gfx--overlays)) + (kitty-gfx--log "remove-overlay: done (remaining=%d)" (length kitty-gfx--overlays)))) ;;;; Public API @@ -565,34 +767,39 @@ BEG/END span the overlay region. MAX-COLS/MAX-ROWS limit size." (let* ((max-c (or max-cols kitty-gfx-max-width)) (max-r (or max-rows kitty-gfx-max-height)) (abs-file (expand-file-name file)) - (cached (gethash abs-file kitty-gfx--image-cache)) - (image-id (if cached (car cached) (kitty-gfx--alloc-id))) - (dims (cond - (cached (cdr cached)) - (t (let ((px (kitty-gfx--image-pixel-size abs-file))) - (if px - (kitty-gfx--compute-cell-dims - (car px) (cdr px) max-c max-r) - (cons (min 40 max-c) (min 15 max-r))))))) + (cached-id (kitty-gfx--cache-get abs-file)) + (image-id (or cached-id (kitty-gfx--alloc-id))) + ;; Always compute dimensions fresh — they depend on max-cols/rows + ;; which vary by display context (org inline vs image-mode vs dired). + (dims (let ((px (kitty-gfx--image-pixel-size abs-file))) + (if px + (kitty-gfx--compute-cell-dims + (car px) (cdr px) max-c max-r) + (cons (min 40 max-c) (min 15 max-r))))) (cols (car dims)) (rows (cdr dims)) (start (or beg (point))) (stop (or end (point)))) (kitty-gfx--log "display-image: file=%s id=%d cols=%d rows=%d beg=%s end=%s cached=%s" - abs-file image-id cols rows start stop (if cached "yes" "no")) + abs-file image-id cols rows start stop (if cached-id "yes" "no")) ;; Transmit image if not cached - (unless cached + (unless cached-id (let* ((png (kitty-gfx--convert-to-png abs-file)) - (b64 (kitty-gfx--read-file-base64 png))) - (kitty-gfx--log "transmit: id=%d b64-len=%d png=%s" image-id (length b64) png) - (kitty-gfx--transmit-image image-id b64) - (puthash abs-file (cons image-id dims) kitty-gfx--image-cache) - (when (and png (not (string= png abs-file))) - (delete-file png t)))) - ;; Create overlay with blank space - (kitty-gfx--make-overlay start stop image-id cols rows) - ;; Schedule initial render - (kitty-gfx--schedule-refresh))) + (temp-p (and png (not (string= png abs-file))))) + (unwind-protect + (let ((b64 (when png (kitty-gfx--read-file-base64 png)))) + (if (not b64) + (kitty-gfx--log "display-image: skipped %s (conversion failed)" abs-file) + (kitty-gfx--log "transmit: id=%d b64-len=%d png=%s" image-id (length b64) png) + (kitty-gfx--transmit-image image-id b64) + (kitty-gfx--cache-put abs-file image-id))) + (when temp-p + (ignore-errors (delete-file png)))))) + ;; Create overlay with blank space (even for cached images, dims are fresh) + (when (or cached-id (gethash abs-file kitty-gfx--image-cache)) + (kitty-gfx--make-overlay start stop image-id cols rows) + ;; Schedule initial render + (kitty-gfx--schedule-refresh)))) (defun kitty-gfx--display-image-centered (file max-cols max-rows &optional win-cols win-rows @@ -627,31 +834,40 @@ The buffer should be writable (caller handles `inhibit-read-only')." (insert (make-string h-pad ?\s)) (let* ((img-start (point)) (_ (insert "\n")) - ;; Ensure image is transmitted (use cache for data, not dims) - (cached (gethash abs-file kitty-gfx--image-cache)) - (image-id (if cached (car cached) (kitty-gfx--alloc-id)))) - (unless cached + ;; Ensure image is transmitted (cache stores only the ID) + (cached-id (kitty-gfx--cache-get abs-file)) + (image-id (or cached-id (kitty-gfx--alloc-id)))) + (unless cached-id (let* ((png (kitty-gfx--convert-to-png abs-file)) - (b64 (kitty-gfx--read-file-base64 png))) - (kitty-gfx--transmit-image image-id b64) - (puthash abs-file (cons image-id (cons img-cols img-rows)) - kitty-gfx--image-cache) - (when (and png (not (string= png abs-file))) - (delete-file png t)))) + (temp-p (and png (not (string= png abs-file))))) + (unwind-protect + (let ((b64 (when png (kitty-gfx--read-file-base64 png)))) + (if (not b64) + (kitty-gfx--log "centered: skipped %s (conversion failed)" abs-file) + (kitty-gfx--transmit-image image-id b64) + (kitty-gfx--cache-put abs-file image-id))) + (when temp-p + (ignore-errors (delete-file png)))))) ;; Create overlay at the scaled dimensions - (kitty-gfx--make-overlay img-start (point) image-id img-cols img-rows) - (kitty-gfx--schedule-refresh)))) + (when (or cached-id (gethash abs-file kitty-gfx--image-cache)) + (kitty-gfx--make-overlay img-start (point) image-id img-cols img-rows) + (kitty-gfx--schedule-refresh))))) (defun kitty-gfx-remove-images (&optional beg end) "Remove all kitty-gfx overlays in region BEG..END (defaults to whole buffer)." (interactive) - (dolist (ov (overlays-in (or beg (point-min)) (or end (point-max)))) - (when (overlay-get ov 'kitty-gfx) - (kitty-gfx--remove-overlay ov)))) + (let ((count 0)) + (dolist (ov (overlays-in (or beg (point-min)) (or end (point-max)))) + (when (overlay-get ov 'kitty-gfx) + (cl-incf count) + (kitty-gfx--remove-overlay ov))) + (kitty-gfx--log "remove-images: removed %d overlays from %s" count (buffer-name)))) (defun kitty-gfx-clear-all () "Remove all images from all buffers and the terminal." (interactive) + (kitty-gfx--log "clear-all: begin (cache=%d lru=%d)" + (hash-table-count kitty-gfx--image-cache) (length kitty-gfx--cache-lru)) ;; Walk all buffers, not just current (dolist (buf (buffer-list)) (with-current-buffer buf @@ -659,8 +875,10 @@ The buffer should be writable (caller handles `inhibit-read-only')." (kitty-gfx-remove-images)))) (kitty-gfx--delete-all-images) (clrhash kitty-gfx--image-cache) + (setq kitty-gfx--cache-lru nil) (setq kitty-gfx--next-id 1) - (setq kitty-gfx--next-placement-id 1)) + (setq kitty-gfx--next-placement-id 1) + (kitty-gfx--log "clear-all: done (reset IDs to 1)")) ;;;; Minor mode @@ -672,49 +890,60 @@ The buffer should be writable (caller handles `inhibit-read-only')." (if kitty-graphics-mode (if (kitty-gfx--supported-p) (progn + (kitty-gfx--log "mode: enabling") (kitty-gfx--delete-all-images) ; clear stale state (kitty-gfx--query-cell-size) (kitty-gfx--install-hooks) (kitty-gfx--install-integrations) + (kitty-gfx--log "mode: enabled (cell=%dx%d)" + kitty-gfx--cell-pixel-width kitty-gfx--cell-pixel-height) (message "Kitty graphics mode enabled")) + (kitty-gfx--log "mode: terminal not supported, aborting enable") (setq kitty-graphics-mode nil) (message "Kitty graphics: terminal not supported")) + (kitty-gfx--log "mode: disabling") (kitty-gfx--uninstall-hooks) (kitty-gfx--uninstall-integrations) - (kitty-gfx--delete-all-images))) + (kitty-gfx--delete-all-images) + (when kitty-gfx--render-timer + (cancel-timer kitty-gfx--render-timer)) + (setq kitty-gfx--render-timer nil + kitty-gfx--refresh-pending nil) + (kitty-gfx--log "mode: disabled"))) (defun kitty-gfx--install-hooks () "Install redisplay hooks for image refresh." (add-hook 'window-scroll-functions #'kitty-gfx--on-window-scroll) (add-hook 'window-size-change-functions #'kitty-gfx--on-window-change) (add-hook 'window-buffer-change-functions #'kitty-gfx--on-buffer-change) - (add-hook 'post-command-hook #'kitty-gfx--on-redisplay)) + (add-hook 'post-command-hook #'kitty-gfx--on-redisplay) + (add-hook 'kill-buffer-hook #'kitty-gfx--kill-buffer-hook)) (defun kitty-gfx--uninstall-hooks () "Remove redisplay hooks." (remove-hook 'window-scroll-functions #'kitty-gfx--on-window-scroll) (remove-hook 'window-size-change-functions #'kitty-gfx--on-window-change) (remove-hook 'window-buffer-change-functions #'kitty-gfx--on-buffer-change) - (remove-hook 'post-command-hook #'kitty-gfx--on-redisplay)) + (remove-hook 'post-command-hook #'kitty-gfx--on-redisplay) + (remove-hook 'kill-buffer-hook #'kitty-gfx--kill-buffer-hook)) ;;;; Org-mode integration (defun kitty-gfx--on-org-cycle (&rest _args) "Handle org visibility cycling. -Deletes all current-buffer placements from the terminal, clears the -position cache, then schedules a refresh that re-places only the -overlays that are still visible (not inside a fold)." +Deletes image placements from the terminal, clears position caches, +then schedules a refresh that re-places only the overlays that are +still visible (not inside a fold)." + (kitty-gfx--log "on-org-cycle: overlays=%d" (length kitty-gfx--overlays)) (when (and kitty-graphics-mode kitty-gfx--overlays) (kitty-gfx--sync-begin) (unwind-protect (dolist (ov kitty-gfx--overlays) (when (overlay-buffer ov) - ;; Delete this overlay's terminal placement (let ((id (overlay-get ov 'kitty-gfx-id)) (pid (overlay-get ov 'kitty-gfx-pid))) (when (and id pid) (kitty-gfx--delete-placement id pid))) - ;; Clear position cache so visible images get re-placed (overlay-put ov 'kitty-gfx-last-row nil) (overlay-put ov 'kitty-gfx-last-col nil))) (kitty-gfx--sync-end)) @@ -724,7 +953,7 @@ overlays that are still visible (not inside a fold)." "Return non-nil if FILE has an image extension." (let ((ext (file-name-extension file))) (and ext (member (downcase ext) - '("png" "jpg" "jpeg" "gif" "bmp" "svg" + '("png" "jpg" "jpeg" "bmp" "svg" "webp" "tiff" "tif"))))) (defun kitty-gfx--org-display-inline-images-tty (&optional _include-linked beg end) @@ -733,6 +962,7 @@ Scans for file:, attachment:, and relative path links." (when (derived-mode-p 'org-mode) (let ((start (or beg (point-min))) (stop (or end (point-max)))) + (kitty-gfx--log "org-display: scanning region %d..%d in %s" start stop (buffer-name)) (save-restriction (widen) (save-excursion @@ -761,11 +991,15 @@ Scans for file:, attachment:, and relative path links." (not (cl-some (lambda (ov) (overlay-get ov 'kitty-gfx)) (overlays-in link-beg link-end)))) + (kitty-gfx--log "org-display: found link %s at %d..%d" + file link-beg link-end) (condition-case err (kitty-gfx-display-image (expand-file-name file) link-beg link-end kitty-gfx-max-width kitty-gfx-max-height) (error + (kitty-gfx--log "org-display: ERROR %s: %s" + file (error-message-string err)) (message "kitty-gfx: %s: %s" file (error-message-string err)))))))))))))) @@ -773,22 +1007,27 @@ Scans for file:, attachment:, and relative path links." (defun kitty-gfx--org-display-advice (orig-fn &rest args) "Around advice for `org-display-inline-images'." (if (and kitty-graphics-mode (not (display-graphic-p))) - (apply #'kitty-gfx--org-display-inline-images-tty args) + (progn + (kitty-gfx--log "advice: org-display-inline-images (terminal path)") + (apply #'kitty-gfx--org-display-inline-images-tty args)) (apply orig-fn args))) (defun kitty-gfx--org-remove-advice (orig-fn &rest args) "Around advice for `org-remove-inline-images'." (when (and kitty-graphics-mode (not (display-graphic-p))) + (kitty-gfx--log "advice: org-remove-inline-images") (kitty-gfx-remove-images)) (apply orig-fn args)) (defun kitty-gfx--org-toggle-advice (orig-fn &rest args) "Around advice for `org-toggle-inline-images'." (if (and kitty-graphics-mode (not (display-graphic-p))) - (if (cl-some (lambda (ov) (overlay-get ov 'kitty-gfx)) - (overlays-in (point-min) (point-max))) - (kitty-gfx-remove-images) - (kitty-gfx--org-display-inline-images-tty)) + (let ((has-images (cl-some (lambda (ov) (overlay-get ov 'kitty-gfx)) + (overlays-in (point-min) (point-max))))) + (kitty-gfx--log "advice: org-toggle has-images=%s" has-images) + (if has-images + (kitty-gfx-remove-images) + (kitty-gfx--org-display-inline-images-tty))) (apply orig-fn args))) ;; org 10.0+ uses org-link-preview instead of org-toggle-inline-images @@ -897,7 +1136,8 @@ Values > 1.0 zoom in, < 1.0 zoom out.") (max-cols (min win-w kitty-gfx-max-width)) (max-rows (min win-h kitty-gfx-max-height))) ;; Remove existing images and clear buffer - (kitty-gfx--log "image-mode-render: win-w=%d win-h=%d max-cols=%d max-rows=%d" + (kitty-gfx--log "image-mode-render: file=%s scale=%.2f win=%dx%d max=%dx%d" + (file-name-nondirectory file) kitty-gfx--image-scale win-w win-h max-cols max-rows) (kitty-gfx-remove-images) (erase-buffer) @@ -952,6 +1192,13 @@ Values > 1.0 zoom in, < 1.0 zoom out.") (evil-local-set-key 'normal (kbd "0") #'kitty-gfx-image-reset-size) (evil-local-set-key 'normal (kbd "q") #'kill-current-buffer)) (setq-local buffer-read-only t) + ;; Re-render when window size changes (e.g., split/unsplit) + ;; so centering and overflow checks use correct dimensions. + (add-hook 'window-size-change-functions + (lambda (_frame) + (when (eq major-mode 'kitty-gfx-image-mode) + (kitty-gfx--image-mode-render))) + nil t) (kitty-gfx--image-mode-render) (set-buffer-modified-p nil)) (apply orig-fn args))) @@ -969,29 +1216,33 @@ We extract the :file or :data from the image properties." (data (plist-get props :data)) (url (plist-get props :file)) (type (plist-get props :type))) + (kitty-gfx--log "shr-put-image: type=%s url=%s data-len=%s alt=%s" + type url (when data (length data)) alt) (insert (or alt "[image]")) (let ((end (point))) - (condition-case err - (let* ((suffix (cond - ((eq type 'jpeg) ".jpg") - ((eq type 'gif) ".gif") - ((eq type 'webp) ".webp") - ((eq type 'svg) ".svg") - (t ".png"))) - (file (cond - (url (when (file-exists-p url) url)) - (data - (let ((tmp (make-temp-file "kitty-shr-" nil suffix))) - (with-temp-file tmp - (set-buffer-multibyte nil) - (insert data)) - tmp)))) - (temp-p (and data file))) - (when file - (kitty-gfx-display-image file start end) - (when temp-p (delete-file file t)))) - (error - (kitty-gfx--log "shr-put-image error: %s" (error-message-string err)))))) + (let* ((suffix (cond + ((eq type 'jpeg) ".jpg") + ((eq type 'gif) ".gif") + ((eq type 'webp) ".webp") + ((eq type 'svg) ".svg") + (t ".png"))) + (file (cond + (url (when (file-exists-p url) url)) + (data + (let ((tmp (make-temp-file "kitty-shr-" nil suffix))) + (with-temp-file tmp + (set-buffer-multibyte nil) + (insert data)) + tmp)))) + (temp-p (and data file))) + (unwind-protect + (condition-case err + (when file + (kitty-gfx-display-image file start end)) + (error + (kitty-gfx--log "shr-put-image error: %s" (error-message-string err)))) + (when temp-p + (ignore-errors (delete-file file))))))) (apply orig-fn spec alt args))) ;;;; doc-view integration @@ -1026,6 +1277,7 @@ Displays the page image via Kitty graphics instead of an Emacs image spec. FILE is the path to the page PNG." (if (and kitty-graphics-mode (not (display-graphic-p))) (when (and file (file-exists-p file)) + (kitty-gfx--log "doc-view-insert: file=%s scale=%.2f" file kitty-gfx--doc-view-scale) ;; Remember current file for zoom commands (setq kitty-gfx--doc-view-current-file file) ;; Remove previous page image @@ -1076,6 +1328,7 @@ Press `q' in the preview buffer to close it." (unless (derived-mode-p 'dired-mode) (user-error "Not in a dired buffer")) (let ((file (dired-get-file-for-visit))) + (kitty-gfx--log "dired-preview: %s" file) (unless (kitty-gfx--image-file-p file) (user-error "Not an image file")) (let* ((buf-name (format "*kitty-preview: %s*" (file-name-nondirectory file))) @@ -1118,6 +1371,7 @@ Returns a buffer recipe, or nil if not in terminal or not an image." (when (and kitty-graphics-mode (not (display-graphic-p)) (kitty-gfx--supported-p)) + (kitty-gfx--log "dirvish-preview: %s" file) (let* ((buf-name (format " *kitty-dirvish: %s*" (file-name-nondirectory file))) (buf (get-buffer-create buf-name)) (max-cols (min (- (window-width preview-window) 2) kitty-gfx-max-width)) @@ -1234,17 +1488,55 @@ Registers `kitty-image' dispatcher and prepends it to the dispatcher list." ;;;; Buffer cleanup +(defun kitty-gfx--image-id-in-other-buffers-p (id &optional exclude-buf) + "Non-nil if image ID is used by overlays in buffers other than EXCLUDE-BUF. +EXCLUDE-BUF defaults to the current buffer." + (let ((skip (or exclude-buf (current-buffer))) + (found nil)) + (dolist (buf (buffer-list)) + (unless (or found (eq buf skip)) + (with-current-buffer buf + (dolist (ov kitty-gfx--overlays) + (when (and (not found) + (overlay-buffer ov) + (eql (overlay-get ov 'kitty-gfx-id) id)) + (setq found t)))))) + found)) + (defun kitty-gfx--kill-buffer-hook () - "Clean up images when buffer is killed." + "Clean up images when buffer is killed. +Deletes terminal-side placements for this buffer's overlays. +Only deletes terminal-side image data (and cache entries) if no +other buffer has overlays referencing the same image ID — this +prevents breaking shared images (e.g., same file open in org-mode +and image-mode simultaneously)." (when (and kitty-graphics-mode kitty-gfx--overlays) - (dolist (ov kitty-gfx--overlays) - (condition-case nil - (when-let ((id (overlay-get ov 'kitty-gfx-id))) - (kitty-gfx--delete-by-id id)) - (error nil))) - (setq kitty-gfx--overlays nil))) - -(add-hook 'kill-buffer-hook #'kitty-gfx--kill-buffer-hook) + (kitty-gfx--log "kill-buffer-hook: buf=%s overlays=%d" (buffer-name) (length kitty-gfx--overlays)) + (let ((deleted-ids nil)) + (dolist (ov kitty-gfx--overlays) + (condition-case nil + (let ((id (overlay-get ov 'kitty-gfx-id)) + (pid (overlay-get ov 'kitty-gfx-pid))) + (when (and id pid) + ;; Always delete the placement (it's buffer-specific) + (kitty-gfx--delete-placement id pid)) + ;; Only delete the image data if no other buffer uses it + (when (and id (not (memq id deleted-ids))) + (if (kitty-gfx--image-id-in-other-buffers-p id) + (kitty-gfx--log "kill-buffer-hook: id=%d still used in other buffers, keeping" id) + (kitty-gfx--delete-by-id id) + (push id deleted-ids)))) + (error nil))) + ;; Remove cache entries only for IDs we actually deleted + (when deleted-ids + (kitty-gfx--log "kill-buffer-hook: cleaning cache for ids=%S" deleted-ids) + (maphash (lambda (file id) + (when (memq id deleted-ids) + (kitty-gfx--cache-remove file))) + (copy-hash-table kitty-gfx--image-cache))) + (setq kitty-gfx--overlays nil) + (kitty-gfx--log "kill-buffer-hook: done (cache-count=%d)" + (hash-table-count kitty-gfx--image-cache))))) (provide 'kitty-graphics) ;; Local Variables: -- cgit v1.3.1