emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/comint-mime a8b0f67 1/8: Initial commit


From: ELPA Syncer
Subject: [elpa] externals/comint-mime a8b0f67 1/8: Initial commit
Date: Mon, 18 Oct 2021 12:57:17 -0400 (EDT)

branch: externals/comint-mime
commit a8b0f6769d3b93b87a07aa333527f12bc6e3b05f
Author: Augusto Stoffel <arstoffel@gmail.com>
Commit: Augusto Stoffel <arstoffel@gmail.com>

    Initial commit
---
 .gitignore     |   2 +
 README.md      |  93 +++++++++++++++++++++++
 comint-mime.el | 231 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 comint-mime.py |  49 ++++++++++++
 comint-mime.sh |  29 ++++++++
 5 files changed, 404 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a7827ee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.elc
+autoloads.el
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..650c2a7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,93 @@
+comint-mime.el
+==============
+
+This Emacs package provides a mechanism for REPLs (or comint buffers,
+in Emacs parlance) to display graphics and other types of special
+content.
+
+![comint-mime in Python][python]
+
+The main motivation behind this package is to display plots in the
+Python shell.  However, it does more that that.
+
+First, it is not constrained to graphics, and can display other “MIME
+attachments” such as HTML and LaTeX content.  In fact, the Python
+backend of the package implements IPython's [rich display
+interface][ipython_repr].  A use-case beyond the displaying of
+graphics would be to render dataframes as HTML tables; this opens up
+the possibility of typographical improvements over the usual pure-text
+representation.  You can also easily define rich representations for
+your own classes.
+
+Second, the package defines a flexible communication protocol between
+Emacs and the inferior process, and, consequently, can be extended to
+other comint types.  Currently, besides Python, there is support for
+the regular (Unix) shell.  In this case, a special command, `mimecat`,
+is provided to display content.  Again, this works for images, HTML,
+LaTeX snippets, etc.
+
+![comint-mime in Bash][bash]
+
+Usage
+-----
+
+To start enjoying comint-mime, simply call `M-x comint-mime-setup`
+from a supported buffer (which, at the moment, are the `M-x shell` and
+`M-x run-python` buffers).  To apply this permanently, add that same
+function to the appropriate mode hook:
+
+``` elisp
+(add-hook 'shell-mode-hook 'comint-mime-setup)
+(add-hook 'inferior-python-mode-hook 'comint-mime-setup)
+```
+
+Note that for Python it is important to use the IPython interpreter.
+It can be configured to have the same look-and-feel as the classic
+`python` program as follows.
+
+``` elisp
+(when (executable-find "ipython3")
+  (setq python-shell-interpreter "ipython3"
+        python-shell-interpreter-args "--simple-prompt --classic"))
+```
+
+Extending
+---------
+
+To add support for new MIME types, see `comint-mime-renderer-alist`.
+
+To add support for new comints, an entry should be added to
+`comint-mime-setup-function-alist`.  This function should arrange for
+the inferior process to emit an escape sequence whenever some MIME
+content is to be displayed.
+
+The escape sequence has the following shape:
+
+```
+ESC ] 5 1 5 1 ; header LF payload ESC \
+```
+
+Here, `header` is a JSON object containing, at least, the entry
+`type`, which should be the name of a MIME type.  Other header entries
+can be passed; the interpretation is up to the rendering function.
+
+The `payload` can be either the content of the attachment, encoded in
+base64 (which is decoded before being passed to the selected
+renderer), or a `file://` URL (whose content is read and passed to the
+renderer), or yet a `tmpfile://` URL, which indicates that the file
+should be deleted after it is read.
+
+Note that it can take considerable time to insert large amounts of
+data in a comint buffer, specially if it contains long lines.
+Consider using a temporary file for large data transfers.
+
+Todos
+-----
+
+- [ ] It should be possible to support at least Matplotlib in the
+      classic `python` interpreter.
+- [ ] Improve the HTML rendering for numeric tables
+
+[python]: 
https://user-images.githubusercontent.com/6500902/133823411-ca75122d-4a39-4e3c-ac55-b2a1f974ff5e.png
+[bash]: 
https://user-images.githubusercontent.com/6500902/133823494-696ee5a7-f0b0-47a3-9ccb-29ab9f36c3a9.png
+[ipython_repr]: 
https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display
diff --git a/comint-mime.el b/comint-mime.el
new file mode 100644
index 0000000..6071730
--- /dev/null
+++ b/comint-mime.el
@@ -0,0 +1,231 @@
+;;; comint-mime.el --- Display content of various MIME types in comint buffers 
 -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Augusto Stoffel
+
+;; Author: Augusto Stoffel <arstoffel@gmail.com>
+;; Keywords: processes, multimedia
+;; Version: 0
+;; Homepage: https://github.com/astoff/comint-mime
+;; Package-Requires: ((emacs "28.1"))
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package provides a mechanism to display graphics and other
+;; kinds of "MIME attachments" in comint buffers.  The applications
+;; depend on the type of comint.
+;;
+;; In the regular shell, a command `mimecat' becomes available.  It
+;; displays the contents of any file (or standard input) of a
+;; supported format.
+;;
+;; In the Python shell, it is possible to display inline plots, images
+;; and, more generally, alternative representations of any object that
+;; implements IPython's rich display interface.
+;;
+;; To enable comint-mime, simply call `M-x comint-mime-setup' in the
+;; desired comint buffer.  To enable it permanently, add that same
+;; function to an appropriate hook, e.g.
+;;
+;;     (add-hook 'shell-mode-hook 'comint-mime-setup)
+;;     (add-hook 'inferior-python-mode-hook 'comint-mime-setup)
+
+;;; Code:
+
+(require 'json)
+(require 'svg)
+(require 'url-parse)
+
+(defvar comint-mime-enabled-types 'all
+  "MIME types which the inferior process may send to Emacs.
+This is either a list of strings or the symbol `all'.
+
+Note that this merely expresses a preference and its
+interpretation is up to the backend.  The shell, for instance,
+only sends MIME content to Emacs via the mimecat command, so it
+ignores this option altogether.")
+
+(defvar comint-mime-renderer-alist
+  '(("^image/svg+xml\\>" . comint-mime-render-svg)
+    ("^image\\>" . comint-mime-render-image)
+    ("^text/html" . comint-mime-render-html)
+    ("^text/latex" . comint-mime-render-latex)
+    ("^text\\>" . comint-mime-render-plain-text)
+    ("." . comint-mime-render-literally))
+  "Alist associating MIME types to rendering functions.
+
+The keys are interpreted as regexps; the first matching entry is
+chosen.
+
+The values should be functions, to called with a header alist
+and (undecoded) data as arguments and with point at the location
+where the content is to be inserted.")
+
+(defvar comint-mime-setup-function-alist nil
+  "Alist of setup functions for comint-mime.
+The keys should be major modes derived from `comint-mode'.  The
+values should be functions, called by `comint-mime-setup' to
+perform the mode-specific part of the setup.")
+
+(defvar comint-mime-setup-script-dir (if load-file-name
+                                         (file-name-directory load-file-name)
+                                       default-directory)
+  "Directory to look for setup scripts.")
+
+(defun comint-mime-osc-handler (_ text)
+  "Interpret TEXT as an OSC 5151 control sequence.
+This function is intended to be used as an entry of
+`comint-osc-handlers'."
+  (string-match "[^\n]*\n?" text)
+  (let* ((payload (substring text (match-end 0)))
+         (header (json-read-from-string (match-string 0 text)))
+         (data (if (string-match "\\(tmp\\)?file:" payload)
+                   (let* ((tmp (match-beginning 1))
+                          (url (url-generic-parse-url payload))
+                          (file (concat (file-remote-p default-directory)
+                                        (url-filename url))))
+                     (with-temp-buffer
+                       (set-buffer-multibyte nil)
+                       (insert-file-contents-literally file)
+                       (when tmp (delete-file file))
+                       (buffer-substring-no-properties (point-min) 
(point-max))))
+                 (base64-decode-string payload))))
+    (when-let ((fun (cdr (assoc (alist-get 'type header)
+                                comint-mime-renderer-alist
+                                'string-match))))
+      (funcall fun header data))))
+
+;;;###autoload
+(defun comint-mime-setup ()
+  "Enable rendering of MIME types in this comint buffer.
+
+This function can be called in the hook of major modes deriving
+from `comint-mode', or interactively after starting the comint."
+  (interactive)
+  (unless (derived-mode-p 'comint-mode)
+    (user-error "`comint-mime' only makes sense in comint buffers"))
+  (if-let ((fun (cdr (assoc major-mode comint-mime-setup-function-alist
+                            'provided-mode-derived-p))))
+      (progn
+        (add-to-list 'comint-osc-handlers '("5151" . comint-mime-osc-handler))
+        (add-hook 'comint-output-filter-functions 'comint-osc-process-output 
nil t)
+        (funcall fun))
+    (user-error "`comint-mime' is not available for this kind of inferior 
process")))
+
+;;; Renderes
+
+;;;; Images
+(defun comint-mime-render-svg (header data)
+  "Render SVG from HEADER and DATA provided by `comint-mime-osc-handler'."
+  (let ((start (point)))
+    (insert-image (svg-image data))
+    (put-text-property start (point) 'comint-mime header)))
+
+(defun comint-mime-render-image (header data)
+  "Render image from HEADER and DATA provided by `comint-mime-osc-handler'."
+  (let ((start (point)))
+    (insert-image (create-image data nil t))
+    (put-text-property start (point) 'comint-mime header)))
+
+;;;; HTML
+(defun comint-mime-render-html (header data)
+  "Render HTML from HEADER and DATA provided by `comint-mime-osc-handler'."
+  (insert
+   (with-temp-buffer
+     (insert data)
+     (decode-coding-region (point-min) (point-max) 'utf-8)
+     (shr-render-region (point-min) (point-max))
+     ;; Don't let font-lock override those faces
+     (goto-char (point-min))
+     (let (match)
+       (while (setq match (text-property-search-forward 'face))
+         (put-text-property (prop-match-beginning match) (prop-match-end match)
+                            'font-lock-face (prop-match-value match))))
+     (put-text-property (point-min) (point-max) 'comint-mime header)
+     (buffer-string))))
+
+;;;; LaTeX
+(autoload 'org-format-latex "org")
+(defvar org-preview-latex-default-process)
+
+(defun comint-mime-render-latex (header data)
+  "Render LaTeX from HEADER and DATA provided by `comint-mime-osc-handler'."
+  (let ((start (point)))
+    (insert data)
+    (decode-coding-region start (point) 'utf-8)
+    (put-text-property start (point) 'comint-mime header)
+    (save-excursion
+      (org-format-latex "org-ltximg" start (point) default-directory
+                        t nil t org-preview-latex-default-process))))
+
+;;;; Plain text
+(defun comint-mime-render-plain-text (header data)
+  "Render plain text from HEADER and DATA provided by 
`comint-mime-osc-handler'."
+  (let ((start (point)))
+    (insert data)
+    (decode-coding-region start (point) 'utf-8)
+    (put-text-property start (point) 'comint-mime header)))
+
+;;;; Dump without rendering or decoding (for debugging)
+(defun comint-mime-render-literally (header data)
+  "Print HEADER and DATA without special rendering."
+  (print header (current-buffer))
+  (insert data))
+
+;;; Mode-specific setup
+
+;;;; Python
+
+(defvar python-shell--first-prompt-received)
+(declare-function python-shell-send-string-no-output "python.el")
+
+(defun comint-mime-setup-python ()
+  "Setup code specific to `inferior-python-mode'."
+  (if (not python-shell--first-prompt-received)
+      (add-hook 'python-shell-first-prompt-hook #'comint-mime-setup-python nil 
t)
+    (python-shell-send-string-no-output
+     (format "%s\n__COMINT_MIME_setup('''%s''')"
+             (with-temp-buffer
+               (insert-file-contents
+                (expand-file-name "comint-mime.py"
+                                  comint-mime-setup-script-dir))
+               (buffer-string))
+             (if (listp comint-mime-enabled-types)
+                 (string-join comint-mime-enabled-types ";")
+               comint-mime-enabled-types)))))
+
+(push '(inferior-python-mode . comint-mime-setup-python)
+      comint-mime-setup-function-alist)
+
+;;;; Shell
+
+(defun comint-mime-setup-shell (&rest _)
+  "Setup code specific to `shell-mode'."
+  (if (save-excursion
+        (goto-char (field-beginning (point-max) t))
+        (not (re-search-forward comint-prompt-regexp nil t)))
+      (add-hook 'comint-output-filter-functions 'comint-mime-setup-shell nil t)
+    (remove-hook 'comint-output-filter-functions 'comint-mime-setup-shell t)
+    (comint-redirect-send-command
+     (format ". %s\n" (shell-quote-argument
+                       (expand-file-name "comint-mime.sh"
+                                         comint-mime-setup-script-dir)))
+     nil nil t)))
+
+(push '(shell-mode . comint-mime-setup-shell)
+      comint-mime-setup-function-alist)
+
+(provide 'comint-mime)
+;;; comint-mime.el ends here
diff --git a/comint-mime.py b/comint-mime.py
new file mode 100644
index 0000000..86584f4
--- /dev/null
+++ b/comint-mime.py
@@ -0,0 +1,49 @@
+# This file is part of https://github.com/astoff/comint-mime
+
+def __COMINT_MIME_setup(types):
+    try:
+        import IPython, matplotlib
+        ipython = IPython.get_ipython()
+        matplotlib.use('module://ipykernel.pylab.backend_inline')
+    except:
+        print("`comint-mime': error setting up")
+        return
+
+    from base64 import encodebytes
+    from json import dumps as to_json
+    from functools import partial
+
+    OSC = '\033]5151;'
+    ST = '\033\\'
+
+    MIME_TYPES = {
+        "image/png": None,
+        "image/jpeg": None,
+        "text/latex": str.encode,
+        "text/html": str.encode,
+        "application/json": lambda d: to_json(d).encode(),
+    }
+    
+    if types == "all":
+        types = MIME_TYPES
+    else:
+        types = types.split(";")
+
+    def print_osc(type, encoder, data, meta):
+        meta = meta or {}
+        if encoder:
+            data = encoder(data)
+        header = to_json({**meta, "type": type})
+        payload = encodebytes(data).decode()
+        print(f'{OSC}{header}\n{payload}{ST}')
+
+    ipython.display_formatter.active_types = list(MIME_TYPES.keys())
+    for mime, encoder in MIME_TYPES.items():
+        ipython.display_formatter.formatters[mime].enabled = mime in types
+        ipython.mime_renderers[mime] = partial(print_osc, mime, encoder)
+
+    if types:
+        print("`comint-mime' enabled for",
+              ", ".join(t for t in types if t in MIME_TYPES.keys()))
+    else:
+        print("`comint-mime' disabled")
diff --git a/comint-mime.sh b/comint-mime.sh
new file mode 100644
index 0000000..b718aac
--- /dev/null
+++ b/comint-mime.sh
@@ -0,0 +1,29 @@
+# This file is part of https://github.com/astoff/comint-mime
+# shellcheck shell=sh
+
+mimecat () {
+    local type
+    local file
+    case "$1" in
+        -h|--help)
+            echo "Usage: mimecat [-t TYPE] [FILE]"
+            return 0
+            ;;
+        -t|--type)
+            type="$2"
+            shift; shift
+            ;;
+    esac
+    if [ -z "$1" ]; then
+        if [ -z "$type" ]; then
+           echo "mimecat: When reading from stdin, please provide -t TYPE"
+           return 1
+        fi
+        base64 | xargs -0 printf '\033]5151;{"type":"%s"}\n%s\033\\\n' "$type"
+    else
+        file=$(realpath -e "$1") || return 1
+        [ -n "$type" ] || type=$(file -bi "$file")
+        printf '\033]5151;{"type":"%s"}\nfile://%s%s\033\\\n' \
+               "$type" "$(hostname)" "$file"
+    fi
+}



reply via email to

[Prev in Thread] Current Thread [Next in Thread]