riscy / shx-for-emacs

An Emacs shell-mode (and comint-mode) extension that enables displaying small plots and graphics and lets users write shell commands in Emacs Lisp.
GNU General Public License v3.0
224 stars 11 forks source link
comint-mode emacs shell

+TITLE: shx for Emacs

+OPTIONS: toc:3 author:t creator:nil num:nil

+AUTHOR: Chris Rayner

+EMAIL: dchrisrayner@gmail.com

[[https://melpa.org/#/shx][https://melpa.org/packages/shx-badge.svg]] [[https://stable.melpa.org/#/shx][https://stable.melpa.org/packages/shx-badge.svg]] [[https://github.com/riscy/shx-for-emacs/actions][https://github.com/riscy/shx-for-emacs/workflows/test/badge.svg]]

[[file:img/screenshot.png]]

*** Enable automatically If you like shx-mode, you can enable it everywhere:

#+begin_src elisp
(shx-global-mode 1)  ; toggle shx-mode on globally
#+end_src

Now shx will run automatically in any =comint-mode= buffer.  If you don't want
shx to run in every comint-mode buffer, you can use =M-x shx-mode= on a
case-by-case basis, or just add hooks to the mode in question, for example:

#+begin_src elisp
(add-hook 'inferior-python-mode-hook #'shx-mode)
#+end_src

*** Customize Use =M-x customize-group RET shx RET= to see shx's many customization options. Here's an example customization using ~setq~:

+begin_src elisp

(setq
  ;; resync the shell's default-directory with Emacs on "z" commands:
  shx-directory-tracker-regexp "^z "
  ;; vastly improve display performance by breaking up long output lines
  shx-max-output 1024
  ;; prevent input longer than macOS's typeahead buffer from going through
  shx-max-input 1024
  ;; prefer inlined images and plots to have a height of 250 pixels
  shx-img-height 250
  ;; don't show any incidental hint messages about how to use shx
  shx-show-hints nil
  ;; flash the previous comint prompt for a full second when using C-c C-p
  shx-flash-prompt-time 1.0
  ;; use `#' to prefix shx commands instead of the default `:'
  shx-leader "#")
#+end_src

*** General commands | Command | Description | |----------------------+-------------------------------------------------------| | =:alert= | Reveal the buffer with an alert. Useful for markup | | =:clear= | Clear the buffer | | =:date= | Show the date (even when the process is blocked) | | =:diff file1 file2= | Launch an Emacs diff between two files | | =:edit file= | Edit a file. Shortcut: =:e = | | =:eval (elisp-sexp)= | Evaluate some elisp code. Example: =:eval (pwd)= | | =:find = | Run a fuzzy-find for | | =:goto-url = | Completing-read for a URL | | =:header New header= | Change the current ~header-line-format~ | | =:kept regexp= | Show a list of your 'kept' commands matching regexp | | =:keep= | Add the previous command to the list of kept commands | | =:man topic= | Invoke the Emacs man page browser on a topic | | =:ssh = | Restart the shell on the specified host |

There are more than this -- type =:help= for a listing of all user commands.

*** Graphical commands | Command | Description | |------------------------------+------------------------| | =:view image_file.jpg= | Display an image | | =:plotbar data_file.txt= | Display a bar plot | | =:plotline data_file.txt= | Display a line plot | | =:plotmatrix data_file.txt= | Display a heatmap | | =:plotscatter data_file.txt= | Display a scatter plot | | =:plot3d data_file.txt= | Display a 3D plot |

These are for displaying inline graphics and plots in the shell buffer.  You
can control how much vertical space an inline image occupies by customizing
the ~shx-img-height~ variable.

Note =convert= (i.e. ImageMagick) and =gnuplot= need to be installed.  If
the binaries are installed but these commands aren't working, customize the
~shx-path-to-convert~ and ~shx-path-to-gnuplot~ variables to point to the
binaries.  Also note these graphical commands aren't yet compatible with
shells launched on remote hosts (e.g. over ssh or in a Docker container).

*** Asynchronous commands | Command | Description | |-----------------------------------+---------------------------------------------------| | =:delay = | Run a shell command after a specific delay | | =:pulse = | Repeat a shell command forever with a given delay | | =:repeat = | Repeat a shell command ~~ times | | =:stop = | Cancel a repeating or delayed command |

Use these to delay, pulse, or repeat a command a specific number of times.
Unfortunately these only support your typical shell commands, and not shx's
extra (colon-prefixed) commands.  So this possible:
#+begin_src bash
# Run the 'pwd' command 10 seconds from now:
:delay 10 pwd
#+end_src
But this is not possible:
#+begin_src bash
# Run the 'pwd' shx command 10 seconds from now (DOES NOT WORK)
:delay 10 :pwd
#+end_src

* Adding new commands New shx commands are written by defining single-argument elisp functions named ~shx-cmd-COMMAND-NAME~, where ~COMMAND-NAME~ is what the user would type to invoke it. *** Example: a command to rename the buffer If you evaluate the following (or add it to your ~.emacs~),

+begin_src elisp

(defun shx-cmd-rename (name)
  "(SAFE) Rename the current buffer to NAME."
  (if (not (ignore-errors (rename-buffer name)))
      (shx-insert 'error "Can't rename buffer.")
    (shx-insert "Renaming buffer to " name "\n")
    (shx--hint "Emacs won't save buffers starting with *")))
#+end_src
then each shx buffer will immediately have access to the =:rename= command.
When it's invoked, shx will also display a hint about buffer names.

Note the importance of defining a docstring.  This documents the
command so that typing =:help rename= will give the user information on what
the command does.  Further, since the docstring begins with =(SAFE)=,
it becomes part of shx's markup language.  So in this case if:
#+begin_src xml
<rename A new name for the buffer>
#+end_src
appears on a line by itself in the output, the buffer will try to
automatically rename itself.

***** Example: invoking ediff from the shell A command similar to this one is built into shx:

+begin_src elisp

  (defun shx-cmd-diff (files)
    "(SAFE) Launch an Emacs `ediff' between FILES."
    (setq files (shx-tokenize files))
    (if (not (eq (length files) 2))
        (shx-insert 'error "diff <file1> <file2>\n")
      (shx-insert "invoking ediff...\n")
      (shx--asynch-funcall #'ediff (mapcar #'expand-file-name files))))
  #+end_src
  Note that ~files~ is supplied as a string, but it's immediately parsed
  into a list of strings using ~shx-tokenize~.  Helpfully, this function is
  able to parse various styles of quoting and escaping, for example
  ~(shx-tokenize "'file one' file\\ two")~
  evaluates to
  ~("file one" "file two")~.

***** Example: a command to browse URLs If you execute the following,

+begin_src elisp

  (defun shx-cmd-browse (url)
    "Browse the supplied URL."
    (shx-insert "Browsing " 'font-lock-keyword-face url)
    (browse-url url))
  #+end_src
  then each shx buffer will have access to the =:browse= command.

  Note the docstring does not specify that this command is =SAFE=.
  This means =<browse url>= will not become part of shx's markup.  That
  makes sense in this case, since you wouldn't want to give a process the
  power to open arbitrary URLs without prompting.