summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcashmeredev <cashmeresocial@paranoici.org>2026-04-26 21:00:44 +0200
committerGitHub <noreply@github.com>2026-04-26 21:00:44 +0200
commitbcf54dda11f333b217443747f865c803dd7e489d (patch)
tree4a23947b76a86ab2d9d52d7d24ad7ecea861568c
parent4dfd1df3bc1f936ff7119beceae22b19e0c728d3 (diff)
parent8766e973e36117c375ee3ac4ecddbd97a2a12ea9 (diff)
downloadkitty-graphics-bcf54dda11f333b217443747f865c803dd7e489d.tar.gz
Merge pull request #10 from cashmeredev/feat/sixel-encoder-timeout
feat(sixel): configurable encoder + per-invocation timeout
-rw-r--r--AGENTS.md5
-rw-r--r--justfile49
-rw-r--r--kitty-graphics.el140
3 files changed, 176 insertions, 18 deletions
diff --git a/AGENTS.md b/AGENTS.md
index e5c07a6..ccd185f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -265,7 +265,10 @@ Key design differences from images:
- **Required**: Emacs >= 27.1, a supported terminal (Kitty/WezTerm/Ghostty
for Kitty backend; foot/Konsole/xterm/mlterm/mintty for Sixel)
-- **Required for Sixel**: ImageMagick (`magick`/`convert`) for encoding
+- **Required for Sixel**: a Sixel encoder on PATH -- `img2sixel`
+ (libsixel, recommended) or ImageMagick (`magick`/`convert`).
+ Override via `kitty-gfx-sixel-encoder-program'; bound by
+ `kitty-gfx-sixel-encoder-timeout' (default 5s).
- **Optional for Kitty**: ImageMagick for non-PNG formats and pixel-accurate sizing
- **Optional for typst preview**: `typst` CLI on PATH (used by
`kitty-gfx-typst-preview`)
diff --git a/justfile b/justfile
index 1202ca5..22fe756 100644
--- a/justfile
+++ b/justfile
@@ -98,6 +98,55 @@ typst-show fragment="$integral_(-oo)^(+oo) e^(-x^2) dif x = sqrt(pi)$":
echo "png=$png"; \
[ -n "$png" ] && xdg-open "$png"
+# --- Headless sixel checks --------------------------------------------------
+
+# Show resolved sixel encoder (auto-detect: img2sixel > magick > convert)
+sixel-encoder:
+ {{EMACS}} -Q -batch -L . -l {{SRC}} --eval '(princ (format "%S\n" (kitty-gfx--sixel-resolve-encoder)))'
+
+# Encode tests/test-image.png to sixel headlessly, report payload size.
+# Override encoder with: just sixel-encode "img2sixel"
+sixel-encode encoder="":
+ {{EMACS}} -Q -batch -L . -l {{SRC}} --eval '(progn \
+ (setq kitty-gfx-debug t) \
+ (when (> (length "{{encoder}}") 0) \
+ (setq kitty-gfx-sixel-encoder-program "{{encoder}}")) \
+ (princ (format "encoder=%S\n" (kitty-gfx--sixel-resolve-encoder))) \
+ (let ((d (kitty-gfx--sixel-encode "tests/test-image.png" 20 10))) \
+ (princ (format "bytes=%s\n" (and d (length d))))))'
+ @echo "--- log tail ---"
+ @tail -3 /tmp/kitty-gfx.log 2>/dev/null || true
+
+# Verify timeout watchdog kills a hung encoder within `kitty-gfx-sixel-encoder-timeout'
+sixel-timeout-test:
+ @printf '#!/usr/bin/env bash\nsleep 60\n' > /tmp/kgfx-fake-encoder.sh
+ @chmod +x /tmp/kgfx-fake-encoder.sh
+ time {{EMACS}} -Q -batch -L . -l {{SRC}} --eval '(progn \
+ (setq kitty-gfx-debug t \
+ kitty-gfx-sixel-encoder-program "/tmp/kgfx-fake-encoder.sh" \
+ kitty-gfx-sixel-encoder-timeout 1.0) \
+ (with-temp-buffer \
+ (set-buffer-multibyte nil) \
+ (princ (format "ok=%S\n" (kitty-gfx--sixel-run-encoder \
+ "/tmp/kgfx-fake-encoder.sh" 1.0 \
+ (current-buffer) nil)))))'
+ @echo "--- log tail ---"
+ @tail -3 /tmp/kitty-gfx.log 2>/dev/null || true
+ @rm -f /tmp/kgfx-fake-encoder.sh
+
+# --- Interactive sixel tests (run inside a sixel-capable terminal) ----------
+
+# Open test-image.png with sixel backend forced (foot/Konsole/mintty/WezTerm)
+test-sixel-image encoder="":
+ @echo ">> Run inside foot, Konsole, mintty, mlterm, or WezTerm."
+ TERM={{TERM_}} {{EMACS}} -nw -Q -l {{SRC}} \
+ --eval "(setq kitty-gfx-debug t kitty-gfx-preferred-protocol 'sixel)" \
+ $(if [ -n "{{encoder}}" ]; then echo "--eval (setq\\ kitty-gfx-sixel-encoder-program\\ \"{{encoder}}\")"; fi) \
+ --eval "(kitty-graphics-mode 1)" \
+ tests/test-image.png
+
+# --- Logs -------------------------------------------------------------------
+
# Tail the kitty-gfx debug log (set kitty-gfx-debug to t to populate)
log:
tail -f /tmp/kitty-gfx.log
diff --git a/kitty-graphics.el b/kitty-graphics.el
index 78eaebd..640454b 100644
--- a/kitty-graphics.el
+++ b/kitty-graphics.el
@@ -185,6 +185,28 @@ Default is `auto'."
(const :tag "Sixel protocol" sixel))
:group 'kitty-graphics)
+(defcustom kitty-gfx-sixel-encoder-program nil
+ "Program used to encode raster images to Sixel.
+When nil, auto-detect: prefer `img2sixel' (libsixel), then
+`magick' (ImageMagick 7), then `convert' (ImageMagick 6, deprecated).
+When set to a string, use that program and treat it as `img2sixel'-style
+unless the basename is `magick' or `convert'."
+ :type '(choice (const :tag "Auto-detect" nil) string)
+ :group 'kitty-graphics)
+
+(defcustom kitty-gfx-sixel-encoder-args nil
+ "Extra arguments passed to `kitty-gfx-sixel-encoder-program'.
+These come before the per-invocation size flags and the input file."
+ :type '(repeat string)
+ :group 'kitty-graphics)
+
+(defcustom kitty-gfx-sixel-encoder-timeout 5.0
+ "Maximum time in seconds to wait for a single Sixel encoder run.
+When nil, wait indefinitely. Encoders that hang on a malformed image
+will otherwise block Emacs."
+ :type '(choice (const :tag "No timeout" nil) number)
+ :group 'kitty-graphics)
+
(defcustom kitty-gfx-heading-scales '((1 . 2.0) (2 . 1.5) (3 . 1.2))
"Alist mapping org heading level to visual scale factor.
Headings at levels not listed use scale 1.0 (normal size).
@@ -587,35 +609,119 @@ OV is the overlay (unused by Kitty backend but part of the interface)."
(hash (md5 key)))
(expand-file-name (concat "kitty-gfx-sixel-" hash ".six") temporary-file-directory)))
+(defun kitty-gfx--sixel-resolve-encoder ()
+ "Resolve the Sixel encoder program.
+Return a cons (KIND . ABS-PATH) where KIND is `img2sixel' or `imagemagick'
+and ABS-PATH is the resolved executable path, or nil when no encoder
+is available."
+ (let* ((user kitty-gfx-sixel-encoder-program)
+ (path (cond
+ (user (executable-find user))
+ (t (or (executable-find "img2sixel")
+ (executable-find "magick")
+ (executable-find "convert"))))))
+ (when path
+ (let ((base (downcase (file-name-nondirectory path))))
+ (cons (cond
+ ((string-prefix-p "img2sixel" base) 'img2sixel)
+ ((or (string-prefix-p "magick" base)
+ (string-prefix-p "convert" base)) 'imagemagick)
+ ;; User-specified non-standard binary: assume img2sixel-style
+ (t 'img2sixel))
+ path)))))
+
+(defun kitty-gfx--sixel-run-encoder (program timeout dest-buffer args)
+ "Run PROGRAM with ARGS, writing stdout into DEST-BUFFER.
+TIMEOUT, when a positive number, terminates the process after that many
+seconds and signals an error. Errors include captured stderr.
+Returns t on success, nil on failure (failures are logged, not signalled)."
+ (let* ((stderr-buf (generate-new-buffer " *kitty-gfx-sixel-stderr*"))
+ (process-connection-type nil)
+ (proc (make-process :name "kitty-gfx-sixel-encoder"
+ :buffer dest-buffer
+ :command (cons program args)
+ :coding 'binary
+ :connection-type 'pipe
+ :stderr stderr-buf
+ :noquery t))
+ (timer (and (numberp timeout)
+ (> timeout 0)
+ (run-at-time
+ timeout nil
+ (lambda (p)
+ (when (process-live-p p)
+ (process-put p 'kitty-gfx-timed-out t)
+ (delete-process p)))
+ proc))))
+ (set-process-sentinel proc #'ignore)
+ (unwind-protect
+ (progn
+ (while (process-live-p proc)
+ (accept-process-output proc 0.1))
+ (let* ((timed-out (process-get proc 'kitty-gfx-timed-out))
+ (exit (process-exit-status proc))
+ (stderr (string-trim
+ (with-current-buffer stderr-buf (buffer-string)))))
+ (cond
+ (timed-out
+ (kitty-gfx--log
+ "sixel-encode: TIMEOUT after %.1fs (%s killed)"
+ (float timeout) program)
+ nil)
+ ((eq exit 0) t)
+ (t
+ (kitty-gfx--log "sixel-encode: %s exit=%s%s"
+ program exit
+ (if (string-empty-p stderr)
+ ""
+ (concat ": " stderr)))
+ nil))))
+ (when timer (cancel-timer timer))
+ (when (buffer-live-p stderr-buf) (kill-buffer stderr-buf)))))
+
(defun kitty-gfx--sixel-encode (png-file cols rows)
"Encode PNG-FILE as Sixel data for COLS x ROWS cells.
Returns Sixel data string or nil on failure.
-Computes pixel dimensions from cell size."
+Computes pixel dimensions from cell size. The encoder is selected via
+`kitty-gfx-sixel-encoder-program' (auto-detected when nil) and bounded
+by `kitty-gfx-sixel-encoder-timeout'."
(let* ((cw (or kitty-gfx--cell-pixel-width 8))
(ch (or kitty-gfx--cell-pixel-height 16))
(pixel-w (* cols cw))
(pixel-h (* rows ch))
- (convert (or (executable-find "magick") (executable-find "convert"))))
- (if (not convert)
+ (resolved (kitty-gfx--sixel-resolve-encoder)))
+ (if (not resolved)
(progn
- (kitty-gfx--log "sixel-encode: no ImageMagick, cannot encode")
- (message "kitty-gfx: Sixel backend requires ImageMagick")
+ (kitty-gfx--log "sixel-encode: no encoder found (img2sixel/magick/convert)")
+ (message "kitty-gfx: Sixel backend requires img2sixel or ImageMagick")
nil)
- (when (string-suffix-p "convert" convert)
- (kitty-gfx--log "sixel-encode: WARNING deprecated `convert' binary resolved: %s (use `magick' instead)" convert))
- (kitty-gfx--log "sixel-encode: %s -> %dx%d pixels via %s"
- png-file pixel-w pixel-h convert)
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (let* ((args (list convert nil '(t nil) nil
- png-file "-geometry" (format "%dx%d" pixel-w pixel-h)
- "sixel:-"))
- (exit-code (apply #'call-process args)))
- (if (zerop exit-code)
+ (let* ((kind (car resolved))
+ (path (cdr resolved))
+ (base (file-name-nondirectory path))
+ (args (pcase kind
+ ('img2sixel
+ (append kitty-gfx-sixel-encoder-args
+ (list "-w" (number-to-string pixel-w)
+ "-h" (number-to-string pixel-h))
+ (list png-file)))
+ ('imagemagick
+ (append (list png-file)
+ kitty-gfx-sixel-encoder-args
+ (list "-geometry"
+ (format "%dx%d" pixel-w pixel-h))
+ (list "sixel:-"))))))
+ (when (string-prefix-p "convert" (downcase base))
+ (kitty-gfx--log "sixel-encode: WARNING deprecated `convert' resolved (%s); install `magick' or `img2sixel'" path))
+ (kitty-gfx--log "sixel-encode: %s -> %dx%d pixels via %s (%s)"
+ png-file pixel-w pixel-h base kind)
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (if (kitty-gfx--sixel-run-encoder
+ path kitty-gfx-sixel-encoder-timeout
+ (current-buffer) args)
(let ((data (buffer-string)))
(kitty-gfx--log "sixel-encode: success (%d bytes)" (length data))
data)
- (kitty-gfx--log "sixel-encode: FAILED exit=%d" exit-code)
nil))))))
(defun kitty-gfx--sixel-prepare (file _image-id)