diff options
Diffstat (limited to 'kitty-graphics.el')
| -rw-r--r-- | kitty-graphics.el | 525 |
1 files changed, 430 insertions, 95 deletions
diff --git a/kitty-graphics.el b/kitty-graphics.el index 7ae9cb7..7fafd62 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -895,13 +895,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. @@ -1403,14 +1404,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. @@ -1433,13 +1439,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)))))))) @@ -1511,6 +1529,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 @@ -1526,7 +1557,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))))) @@ -1542,18 +1573,22 @@ 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)) - (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 @@ -1576,24 +1611,29 @@ Dispatches to heading or image 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-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))) @@ -1678,15 +1718,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)) @@ -1694,6 +1728,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 @@ -1709,27 +1744,36 @@ 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). 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. - ;; 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))) + (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. (when kitty-gfx--render-timer @@ -1921,6 +1965,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 @@ -1936,17 +2056,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)))) @@ -2029,9 +2146,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) @@ -2661,8 +2779,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) @@ -2670,15 +2793,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))) @@ -2693,19 +2819,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'." @@ -2815,6 +2941,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 @@ -2831,15 +3111,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) - (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)))) + ;; 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) + (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))) @@ -3270,7 +3569,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) @@ -3295,6 +3618,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) |
