summaryrefslogtreecommitdiff
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
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.
-rw-r--r--AGENTS.md16
-rw-r--r--justfile103
-rw-r--r--kitty-graphics.el151
-rw-r--r--tests/test-typst.typ31
4 files changed, 301 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 62d8813..e5c07a6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -48,6 +48,16 @@ TERM=xterm-256color emacs -nw -Q -l kitty-graphics.el \
# Then: C-c C-x C-v (org-toggle-inline-images)
```
+### Test typst inline equations
+
+```sh
+TERM=xterm-256color emacs -nw -Q -l kitty-graphics.el \
+ --eval "(kitty-graphics-mode 1)" tests/test-typst.typ
+# Then: M-x kitty-gfx-typst-preview
+# To clear: M-x kitty-gfx-typst-clear-preview
+# Requires the `typst` CLI on PATH.
+```
+
### Test text sizing (org heading scales)
```sh
@@ -241,6 +251,10 @@ Key design differences from images:
- **Limitation**: Sixel is 256 colors (vs truecolor on Kitty), stateless (re-emits on scroll)
- **Done**: LaTeX fragment preview in org-mode (#3)
- **Done**: doc-view / pdf-view-mode integration (#4)
+- **Done**: Typst inline equation preview (#5) -- `kitty-gfx-typst-preview`
+ scans `$...$' fragments in the current buffer or region, compiles each
+ via the `typst' CLI, and displays the resulting PNG inline. No advice
+ hooks; bind to a key or invoke via `M-x'.
- **Done**: Text sizing protocol for org heading sizes (OSC 66, Kitty >= 0.40.0)
- **Done**: Sixel backend with auto-detection (v0.3.0)
- **WIP**: Sixel image misplacement in org-mode (reported in foot terminal)
@@ -253,6 +267,8 @@ Key design differences from images:
for Kitty backend; foot/Konsole/xterm/mlterm/mintty for Sixel)
- **Required for Sixel**: ImageMagick (`magick`/`convert`) for encoding
- **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`)
- **No external Elisp deps** beyond built-in `cl-lib`
## Versioning
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..1202ca5
--- /dev/null
+++ b/justfile
@@ -0,0 +1,103 @@
+# kitty-graphics.el -- developer task runner
+#
+# Requires: just, emacs, typst (for typst tests), imagemagick (for sixel).
+# Run `just` to list recipes.
+
+set shell := ["bash", "-cu"]
+
+EMACS := env_var_or_default("EMACS", "emacs")
+TERM_ := env_var_or_default("KGFX_TERM", "xterm-256color")
+SRC := "kitty-graphics.el"
+
+# Default: list recipes
+default:
+ @just --list --unsorted
+
+# --- Build / lint -----------------------------------------------------------
+
+# Byte-compile (primary lint check)
+compile:
+ rm -f {{SRC}}c
+ {{EMACS}} -Q -batch -f batch-byte-compile {{SRC}}
+
+# Byte-compile, treat warnings as errors
+lint:
+ rm -f {{SRC}}c
+ {{EMACS}} -Q -batch --eval '(setq byte-compile-error-on-warn t)' \
+ -f batch-byte-compile {{SRC}}
+
+# Load-test: file evaluates without error
+load:
+ {{EMACS}} -Q -batch -l {{SRC}} -f kill-emacs
+
+# Remove generated artifacts
+clean:
+ rm -f {{SRC}}c
+ rm -rf /tmp/kitty-gfx-typst /tmp/kitty-gfx-sixel-*.six /tmp/kitty-gfx.log
+
+# --- Interactive tests (open terminal Emacs) --------------------------------
+
+# Test typst inline equations (M-x kitty-gfx-typst-preview after open)
+test-typst:
+ @echo ">> M-x kitty-gfx-typst-preview to render"
+ @echo ">> M-x kitty-gfx-typst-clear-preview to clear"
+ TERM={{TERM_}} {{EMACS}} -nw -Q -l {{SRC}} \
+ --eval "(kitty-graphics-mode 1)" \
+ --eval "(setq kitty-gfx-debug t)" \
+ tests/test-typst.typ
+
+# Test org-mode inline images -- C-c C-x C-v after open
+test-org:
+ TERM={{TERM_}} {{EMACS}} -nw -Q -l {{SRC}} \
+ --eval "(kitty-graphics-mode 1)" \
+ tests/test-kitty-gfx.org
+
+# Test text sizing protocol (OSC 66) on org headings
+test-headings:
+ TERM={{TERM_}} {{EMACS}} -nw -Q -l {{SRC}} \
+ --eval "(setq kitty-gfx-heading-sizes-auto t)" \
+ --eval "(kitty-graphics-mode 1)" \
+ tests/test-kitty-gfx.org
+
+# Test image-mode rendering
+test-image:
+ TERM={{TERM_}} {{EMACS}} -nw -Q -l {{SRC}} \
+ --eval "(kitty-graphics-mode 1)" \
+ tests/test-image.png
+
+# Test doc-view / PDF rendering
+test-pdf:
+ TERM={{TERM_}} {{EMACS}} -nw -Q -l {{SRC}} \
+ --eval "(kitty-graphics-mode 1)" \
+ tests/test-document.pdf
+
+# Test markdown-overlays integration
+test-markdown:
+ TERM={{TERM_}} {{EMACS}} -nw -Q -l {{SRC}} \
+ --eval "(kitty-graphics-mode 1)" \
+ tests/test-markdown.md
+
+# Test LaTeX fragment preview in org-mode (C-c C-x C-l on a fragment)
+test-latex:
+ TERM={{TERM_}} {{EMACS}} -nw -Q -l {{SRC}} \
+ --eval "(kitty-graphics-mode 1)" \
+ tests/test-kitty-gfx.org
+
+# --- Headless typst checks --------------------------------------------------
+
+# Compile a typst fragment headlessly, print the PNG path
+typst-render fragment="$x^2 + y^2 = z^2$":
+ {{EMACS}} -Q -batch -L . -l {{SRC}} --eval '(progn \
+ (setq kitty-gfx-debug t) \
+ (let ((png (kitty-gfx--typst-render "{{fragment}}"))) \
+ (princ (format "png=%s exists=%s\n" png (and png (file-exists-p png))))))'
+
+# Render fragment and open PNG with xdg-open
+typst-show fragment="$integral_(-oo)^(+oo) e^(-x^2) dif x = sqrt(pi)$":
+ @png=$({{EMACS}} -Q -batch -L . -l {{SRC}} --eval '(princ (kitty-gfx--typst-render "{{fragment}}"))' 2>/dev/null); \
+ echo "png=$png"; \
+ [ -n "$png" ] && xdg-open "$png"
+
+# 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 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
diff --git a/tests/test-typst.typ b/tests/test-typst.typ
new file mode 100644
index 0000000..a28fcbf
--- /dev/null
+++ b/tests/test-typst.typ
@@ -0,0 +1,31 @@
+// Test fixture for kitty-graphics.el typst inline equation preview.
+//
+// Usage:
+// TERM=xterm-256color emacs -nw -Q -l kitty-graphics.el \
+// --eval "(kitty-graphics-mode 1)" tests/test-typst.typ
+// M-x kitty-gfx-typst-preview
+// M-x kitty-gfx-typst-clear-preview
+//
+// Inline math fragments below should each be replaced by a rendered
+// PNG overlay. Escaped dollar signs (\$ ... \$) must be ignored.
+
+= Inline samples
+
+Pythagoras: $a^2 + b^2 = c^2$, classic.
+
+Euler: $e^(i pi) + 1 = 0$ -- the famous identity.
+
+Display style with surrounding spaces:
+$ integral_(-oo)^(+oo) e^(-x^2) dif x = sqrt(pi) $
+
+Mixed line: $x = 1$ and $y = 2$ on the same line.
+
+Escaped dollar signs should be skipped: \$not math\$ here.
+
+= Edge cases
+
+Sum: $sum_(k=0)^n binom(n, k) = 2^n$.
+
+Matrix: $mat(1, 2; 3, 4)$.
+
+Greek: $alpha + beta = gamma$.