diff options
| -rw-r--r-- | AGENTS.md | 16 | ||||
| -rw-r--r-- | justfile | 103 | ||||
| -rw-r--r-- | kitty-graphics.el | 151 | ||||
| -rw-r--r-- | tests/test-typst.typ | 31 |
4 files changed, 301 insertions, 0 deletions
@@ -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$. |
