summaryrefslogtreecommitdiff
path: root/kitty-graphics.el
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-04-26 20:36:04 +0200
committercashmere <cashmere@autistici.org>2026-04-26 20:36:04 +0200
commitbaa6cf8cc43ce676b806db01e390668dc4aa6790 (patch)
tree103e8d459f7f3a79dd7985f9b36f8ff263a672ad /kitty-graphics.el
parent7e4faffa32d6f0ef885fceb0a9ecd93b3fe5c43f (diff)
downloadkitty-graphics-baa6cf8cc43ce676b806db01e390668dc4aa6790.tar.gz
feat: add typst inline equation preview
Mirrors the existing LaTeX fragment integration but uses the `typst` CLI to compile `$...$' math fragments into PNGs that are then displayed inline through the existing kitty-graphics overlay pipeline. - New section in kitty-graphics.el: customs (typst-command, ppi, text-size, preamble), SHA-1 cached PNGs under /tmp/kitty-gfx-typst, Emacs face-foreground -> typst rgb() hex conversion. - Two autoloaded commands: `kitty-gfx-typst-preview' and `kitty-gfx-typst-clear-preview', region-aware. - Regex skips backslash-escaped dollars (\\$not math\\$). - justfile with recipes: compile/lint/load + per-feature test-* and headless typst-render / typst-show. - tests/test-typst.typ fixture with inline, display, mixed and edge-case fragments. - AGENTS.md updated: test instructions, dependency note, issue marked done. Closes #7. Parent #5.
Diffstat (limited to 'kitty-graphics.el')
-rw-r--r--kitty-graphics.el151
1 files changed, 151 insertions, 0 deletions
diff --git a/kitty-graphics.el b/kitty-graphics.el
index 7e8accc..78eaebd 100644
--- a/kitty-graphics.el
+++ b/kitty-graphics.el
@@ -2289,6 +2289,157 @@ image via Kitty graphics instead of an Emacs image spec."
(eq (overlay-get ov 'org-overlay-type) 'org-latex-overlay))
(kitty-gfx--remove-overlay ov)))))
+;;;; Typst inline equation preview
+
+(defcustom kitty-gfx-typst-command "typst"
+ "Path to the typst executable used for inline math previews."
+ :type 'string
+ :group 'kitty-graphics)
+
+(defcustom kitty-gfx-typst-ppi 300
+ "Pixels-per-inch passed to `typst compile' for math previews.
+Higher values give crisper images at the cost of compile time."
+ :type 'integer
+ :group 'kitty-graphics)
+
+(defcustom kitty-gfx-typst-text-size 11
+ "Text size in points used when rendering typst math fragments."
+ :type 'integer
+ :group 'kitty-graphics)
+
+(defcustom kitty-gfx-typst-preamble nil
+ "Extra typst code prepended to each math fragment before compilation.
+When nil, a default preamble is used that auto-sizes the page,
+disables the page fill, and applies `kitty-gfx-typst-text-size' with
+the current Emacs foreground color."
+ :type '(choice (const :tag "Default" nil) string)
+ :group 'kitty-graphics)
+
+(defvar kitty-gfx--typst-cache-dir nil
+ "Directory holding cached typst-rendered PNGs.")
+
+(defun kitty-gfx--typst-cache-dir ()
+ "Return the cache directory for typst PNGs, creating it if needed."
+ (unless (and kitty-gfx--typst-cache-dir
+ (file-directory-p kitty-gfx--typst-cache-dir))
+ (setq kitty-gfx--typst-cache-dir
+ (expand-file-name "kitty-gfx-typst" temporary-file-directory))
+ (make-directory kitty-gfx--typst-cache-dir t))
+ kitty-gfx--typst-cache-dir)
+
+(defun kitty-gfx--typst-color-hex (color)
+ "Convert COLOR (name or `#rrggbb') to a `#RRGGBB' string for typst.
+Returns `#000000' on failure."
+ (or (when (stringp color)
+ (let ((rgb (ignore-errors (color-name-to-rgb color))))
+ (when (and rgb (= (length rgb) 3))
+ (format "#%02x%02x%02x"
+ (round (* 255 (nth 0 rgb)))
+ (round (* 255 (nth 1 rgb)))
+ (round (* 255 (nth 2 rgb)))))))
+ "#000000"))
+
+(defun kitty-gfx--typst-default-preamble ()
+ "Build the default typst preamble using current Emacs foreground color."
+ (let* ((raw (face-attribute 'default :foreground nil))
+ (fg (kitty-gfx--typst-color-hex
+ (and (stringp raw)
+ (not (string-prefix-p "unspecified" raw))
+ raw))))
+ (format "#set page(width: auto, height: auto, margin: 2pt, fill: none)
+#set text(size: %dpt, fill: rgb(\"%s\"))
+"
+ kitty-gfx-typst-text-size fg)))
+
+(defconst kitty-gfx--typst-math-regexp
+ "\\$\\(?:[^$\n\\\\]\\|\\\\.\\)+\\$"
+ "Regexp matching a single `$...$' typst math fragment on one line.
+Backslash-escaped characters within the fragment are allowed; newlines
+end the match.")
+
+(defun kitty-gfx--typst-render (fragment)
+ "Compile FRAGMENT (typst source including the surrounding `$') to PNG.
+Return the absolute path to the generated PNG, or nil on failure.
+Results are cached under `kitty-gfx--typst-cache-dir', keyed by SHA-1
+of the full preamble + fragment + ppi."
+ (unless (executable-find kitty-gfx-typst-command)
+ (user-error "typst executable not found: %s" kitty-gfx-typst-command))
+ (let* ((preamble (or kitty-gfx-typst-preamble
+ (kitty-gfx--typst-default-preamble)))
+ (body (concat preamble fragment "\n"))
+ (key (sha1 (format "%s|%d" body kitty-gfx-typst-ppi)))
+ (dir (kitty-gfx--typst-cache-dir))
+ (typ (expand-file-name (concat key ".typ") dir))
+ (png (expand-file-name (concat key ".png") dir)))
+ (unless (file-exists-p png)
+ (with-temp-file typ (insert body))
+ (let* ((log-buf (get-buffer-create "*kitty-gfx-typst*"))
+ (ret (condition-case err
+ (call-process kitty-gfx-typst-command nil log-buf nil
+ "compile"
+ "--format" "png"
+ "--ppi" (number-to-string kitty-gfx-typst-ppi)
+ typ png)
+ (error
+ (kitty-gfx--log "typst-render: call-process error: %S" err)
+ -1))))
+ (unless (eq ret 0)
+ (kitty-gfx--log "typst-render: compile failed (exit=%s) for %s"
+ ret typ)
+ (setq png nil))))
+ (and png (file-exists-p png) png)))
+
+;;;###autoload
+(defun kitty-gfx-typst-preview (&optional beg end)
+ "Render typst `$...$' math fragments as inline PNG images.
+With an active region, restrict to it; otherwise scan the whole buffer.
+Existing typst preview overlays in the range are replaced.
+Requires `kitty-graphics-mode' and a working `typst' executable."
+ (interactive (if (use-region-p)
+ (list (region-beginning) (region-end))
+ (list nil nil)))
+ (unless kitty-graphics-mode
+ (user-error "kitty-graphics-mode is not active"))
+ (unless kitty-gfx--active-backend
+ (user-error "Terminal does not support graphics"))
+ (let ((start (or beg (point-min)))
+ (stop (or end (point-max)))
+ (count 0))
+ (kitty-gfx-typst-clear-preview start stop)
+ (save-excursion
+ (goto-char start)
+ (while (re-search-forward kitty-gfx--typst-math-regexp stop t)
+ (let* ((m-beg (match-beginning 0))
+ (m-end (match-end 0))
+ (frag (match-string-no-properties 0))
+ (png (kitty-gfx--typst-render frag)))
+ (when png
+ (kitty-gfx-display-image png m-beg m-end)
+ (when-let* ((ov (car kitty-gfx--overlays)))
+ (overlay-put ov 'kitty-gfx-typst t)
+ (overlay-put ov 'modification-hooks
+ (list (lambda (o after &rest _)
+ (when after
+ (kitty-gfx--remove-overlay o))))))
+ (cl-incf count)))))
+ (when (called-interactively-p 'any)
+ (message "kitty-gfx: rendered %d typst fragment%s"
+ count (if (= count 1) "" "s")))
+ count))
+
+;;;###autoload
+(defun kitty-gfx-typst-clear-preview (&optional beg end)
+ "Remove kitty-graphics typst preview overlays between BEG and END.
+With an active region, restrict to it; otherwise clear the whole buffer."
+ (interactive (if (use-region-p)
+ (list (region-beginning) (region-end))
+ (list nil nil)))
+ (let ((start (or beg (point-min)))
+ (stop (or end (point-max))))
+ (dolist (ov (overlays-in start stop))
+ (when (overlay-get ov 'kitty-gfx-typst)
+ (kitty-gfx--remove-overlay ov)))))
+
;;;; image-mode integration
(defvar-local kitty-gfx--image-scale 1.0