[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
+}
- [elpa] branch externals/comint-mime created (now d9cdad5), ELPA Syncer, 2021/10/18
- [elpa] externals/comint-mime a8b0f67 1/8: Initial commit,
ELPA Syncer <=
- [elpa] externals/comint-mime 95d9d34 2/8: Improve Matplotlib setup from IPython, ELPA Syncer, 2021/10/18
- [elpa] externals/comint-mime 6b95376 5/8: Reduce Emacs version requirement to 28 (from 28.1), ELPA Syncer, 2021/10/18
- [elpa] externals/comint-mime 3e02807 7/8: Update copyright assignment, ELPA Syncer, 2021/10/18
- [elpa] externals/comint-mime d9cdad5 8/8: Convert README to org format, ELPA Syncer, 2021/10/18
- [elpa] externals/comint-mime e7c847a 6/8: Fix interaction between HTML rendering and process mark, ELPA Syncer, 2021/10/18
- [elpa] externals/comint-mime bb9a1ad 3/8: Hide setup from shell history, at least when "ignorespace" is set, ELPA Syncer, 2021/10/18
- [elpa] externals/comint-mime 9cf131d 4/8: Disable LaTeX rendering by default for now, ELPA Syncer, 2021/10/18