/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.

Primary LanguageEmacs LispGNU General Public License v3.0GPL-3.0

shx for Emacs

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

img/screenshot.png

Table of Contents

Description

shx or “shell-extras” extends comint-mode in Emacs (e.g. M-x shell).

It’s compatible with any underlying REPL (zsh, bash, psql, ipython, etc.).

It parses the output stream in a few useful ways:

  • Display graphics and plots in the shell with a simple markup language (e.g. <view image.png>)
  • Add event-driven and timed behaviors to any shell session
  • Open any filename or URL by arrowing up to it and pressing RET (shx will even try to guess the correct directory)
  • Yank any line to the prompt by arrowing up to it and pressing C-RET
  • Check the time a command was run by mousing over its prompt

shx makes it easy to add new shell commands written in elisp. Some are already built in:

  • :clear clears the buffer (like clear or Command-K on macOS)
  • :e filename.txt opens a file for editing
  • :ssh user@host:port starts a remote shell session using tramp
  • :view image_file.png embeds an image in the shell
  • :plotline data_file.txt embeds a line plot
  • etc.

It also extends shell-mode’s syntax highlighting, recenters and highlights content for better viewing when you run commands like comint-previous-prompt and comint-kill-input, and improves compatibility with evil-mode by anticipating when to switch to insert mode.

Use M-x shx RET to start a new shell session with shx-mode enabled.

This version is tested with Emacs 26.1. Check out the release log.

Install

From MELPA

M-x package-install RET shx RET to install shx from MELPA.

From GNU Guix

guix install emacs-shx to install shx from GNU Guix.

Manually

Add the following to your .emacs:

(add-to-list 'load-path "~/path/to/shx/") ; add shx.el's directory to the load-path
(require 'shx)                            ; load shell-extras

Setup

Quick-start

Type M-x shx RET. Try out the following commands:

  1. :e ~/.bashrc to edit your .bashrc (for example)
  2. :man ls to display the man page for ls
  3. :help to a start a completing read for other shx commands

Enable automatically

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

(shx-global-mode 1)  ; toggle shx-mode on globally

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:

(add-hook 'inferior-python-mode-hook #'shx-mode)

Customize

Use M-x customize-group RET shx RET to see shx’s many customization options. Here’s an example customization using setq:

(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 "#")

Key bindings

Key bindingDescription
C-RETIf the cursor is not on the prompt, paste the current line to the input
RETIf the cursor is on a filename or a URL, try to open it
SPCIf the prompt is :, send SPC straight through to the process
qIf the prompt is :, send q straight through to the process

Note the prompt will be : when reading through the output of less or a man page if you run the following:

(setenv "LESS" "--dumb --prompt=s")

Markup in the shell

shx’s markup can enhance basic command-line applications and drive other events.

If the output ever contains <view mountains.png> on a line by itself, then a scaled rendering of mountains.png will be inlined within the text in the shell. This works because view is a shx command. shx will execute any (safe) shx command that appears with the following syntax:

<command arg1 arg2 ...>

where command is a shx command and arg1 ... argn is a space-separated list of arguments. Arguments don’t need to be surrounded by quotes – the command will figure out how to parse them.

You can use this markup to create a barplot (:plotbar) after collecting some stats, or generate an :alert when a task is finished, and so forth.

Extra shell commands

shx’s ‘extra’ commands are invoked by typing a : followed by the command’s name. (You can change the : prefix by customizing the shx-leader variable.) These commands are written in elisp and so can access all of Emacs’ facilities. Type :help to see a complete listing of shx commands.

One command I use frequently is the :edit (shorthand :e) command:

# edit the .emacs file:
:edit ~/.emacs

# use tramp to edit .emacs on a remote host through ssh:
:e /ssh:remote-host.com:~/.emacs

# use tramp to edit .bashrc on a running docker container:
:e /docker:02fbc948e009:~/.bashrc

# edit a local file as root
:sedit /etc/passwd

Thanks to CeleritasCelery it’s also possible to use environment variables in the argument list:

:e $HOME/.emacs.d

(To see an environment variable’s value, use (getenv "<var>").)

The :ssh and :docker commands are popular for opening “remote” shells:

# open a shell on a remote host:
:ssh user@remote-host.com

# connect to a running docker container
:docker 8a8335d63ff3

# reopen the shell on the localhost:
:ssh

Jordan Besly points out that you can customize the default interpreter for each “remote” using connection-profile-set-local-variables.

I also use the :kept and :keep commands frequently:

# write a complicated command:
wget https://bootstrap.pypa.io/get-pip.py && python get-pip.py

# save the last command:
:keep

# search for commands having to do with pip:
:kept pip

Because these commands are written in elisp, shx gives M-x shell a lot of the same advantages as eshell. You can even evaluate elisp code directly in the buffer (see :help eval).

General commands

CommandDescription
:alertReveal the buffer with an alert. Useful for markup
:clearClear the buffer
:dateShow the date (even when the process is blocked)
:diff file1 file2Launch an Emacs diff between two files
:edit fileEdit a file. Shortcut: :e <file>
:eval (elisp-sexp)Evaluate some elisp code. Example: :eval (pwd)
:find <filename>Run a fuzzy-find for <filename>
:goto-url <url>Completing-read for a URL
:header New headerChange the current header-line-format
:kept regexpShow a list of your ‘kept’ commands matching regexp
:keepAdd the previous command to the list of kept commands
:man topicInvoke the Emacs man page browser on a topic
:ssh <host>Restart the shell on the specified host

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

Graphical commands

CommandDescription
:view image_file.jpgDisplay an image
:plotbar data_file.txtDisplay a bar plot
:plotline data_file.txtDisplay a line plot
:plotmatrix data_file.txtDisplay a heatmap
:plotscatter data_file.txtDisplay a scatter plot
:plot3d data_file.txtDisplay 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

CommandDescription
:delay <sec> <command>Run a shell command after a specific delay
:pulse <sec> <command>Repeat a shell command forever with a given delay
:repeat <count> <sec> <command>Repeat a shell command <count> times
:stop <num>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:

# Run the 'pwd' command 10 seconds from now:
:delay 10 pwd

But this is not possible:

# Run the 'pwd' shx command 10 seconds from now (DOES NOT WORK)
:delay 10 :pwd

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),

(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 *")))

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:

<rename A new name for the buffer>

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:

(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))))

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,

(defun shx-cmd-browse (url)
  "Browse the supplied URL."
  (shx-insert "Browsing " 'font-lock-keyword-face url)
  (browse-url url))

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.

Related

If you’re here, these might be interesting:

And if running a dumb terminal in Emacs isn’t for you, here are some alternatives: