Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/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.
https://github.com/riscy/shx-for-emacs

comint-mode emacs shell

Last synced: 4 days ago
JSON representation

An Emacs shell-mode (and comint-mode) extension that enables displaying small plots and graphics and lets users write shell commands in Emacs Lisp.

Awesome Lists containing this project

README

        

#+TITLE: shx for Emacs
#+OPTIONS: toc:3 author:t creator:nil num:nil
#+AUTHOR: Chris Rayner
#+EMAIL: [email protected]

[[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]]

* Table of Contents :TOC_3_gh:noexport:
- [[#description][Description]]
- [[#install][Install]]
- [[#from-melpa][From MELPA]]
- [[#from-gnu-guix][From GNU Guix]]
- [[#manually][Manually]]
- [[#setup][Setup]]
- [[#quick-start][Quick-start]]
- [[#enable-automatically][Enable automatically]]
- [[#customize][Customize]]
- [[#key-bindings][Key bindings]]
- [[#markup-in-the-shell][Markup in the shell]]
- [[#extra-shell-commands][Extra shell commands]]
- [[#general-commands][General commands]]
- [[#graphical-commands][Graphical commands]]
- [[#asynchronous-commands][Asynchronous commands]]
- [[#adding-new-commands][Adding new commands]]
- [[#related][Related]]

* 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. ==)
- *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 [[https://github.com/riscy/shx-for-emacs/releases][release log]].
* Install
*** From MELPA
=M-x package-install RET shx RET= to install =shx= from [[https://melpa.org/][MELPA]].
*** From GNU Guix
=guix install emacs-shx= to install =shx= from [[https://guix.gnu.org/][GNU Guix]].
*** Manually
Add the following to your =.emacs=:
#+begin_src elisp
(add-to-list 'load-path "~/path/to/shx/") ; add shx.el's directory to the load-path
(require 'shx) ; load shell-extras
#+end_src
* 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:

#+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
* Key bindings
| Key binding | Description |
|-------------+--------------------------------------------------------------------------|
| =C-RET= | If the cursor is not on the prompt, paste the current line to the input |
| =RET= | If the cursor is on a filename or a URL, try to open it |
| =SPC= | If the prompt is =:=, send =SPC= straight through to the process |
| =q= | If 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:
#+begin_src elisp
(setenv "LESS" "--dumb --prompt=s")
#+end_src
* Markup in the shell
shx's markup can enhance basic command-line applications and drive other
events.

If the output ever contains == 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:
#+begin_src xml

#+end_src
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:
#+begin_src bash
# 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
#+end_src

Thanks to [[https://github.com/CeleritasCelery][CeleritasCelery]] it's also possible to use environment variables in
the argument list:
#+begin_src bash
:e $HOME/.emacs.d
#+end_src
(To see an environment variable's value, use ~(getenv "")~.)

The =:ssh= and =:docker= commands are popular for opening "remote" shells:
#+begin_src bash
# open a shell on a remote host:
:ssh [email protected]

# connect to a running docker container
:docker 8a8335d63ff3

# reopen the shell on the localhost:
:ssh
#+end_src
[[https://github.com/p3r7][Jordan Besly]] points out that you can customize the default interpreter
for each "remote" using [[https://www.gnu.org/software/emacs/manual/html_node/tramp/Remote-processes.html][connection-profile-set-local-variables]].

I also use the =:kept= and =:keep= commands frequently:
#+begin_src bash
# 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
#+end_src

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

#+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 \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 == 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:
- [[https://www.masteringemacs.org/article/shell-comint-secrets-history-commands][Shell & Comint Secrets: History commands]]
- [[https://www.masteringemacs.org/article/pcomplete-context-sensitive-completion-emacs][PComplete: Context-Sensitive Completion in Emacs]]
- [[https://dev.to/_darrenburns/10-tools-to-power-up-your-command-line-4id4][10 tools to power up your command line]]
- [[https://www.booleanworld.com/customizing-coloring-bash-prompt/][Creating dynamic bash prompts]]
- [[https://github.com/Orkohunter/keep][The Keep Utility]] inspired the =kept= and =keep= commands
- [[https://terminalsare.sexy/]["Terminals Are Sexy"]] (portal)
- [[https://github.com/riscy/command_line_lint][Command-Line Lint]], another project I maintain
- [[http://ohmyz.sh/][oh my zsh]], a community-driven zsh configuration
- [[https://github.com/Bash-it/bash-it][bash-it]], a community driven bash configuration

And if running a =dumb= terminal in Emacs isn't for you, here are some
alternatives:
- [[https://leanpub.com/the-tao-of-tmux/read][The Tao of tmux]], re: working in the terminal with tmux
- [[https://github.com/zsh-users/zsh-syntax-highlighting][zsh-syntax-highlighting]]
- [[https://hackernoon.com/macbook-my-command-line-utilities-f8a121c3b019#.clz934ly3][Shell configuration tips]] from Vitaly Belman
- [[http://www.iterm2.com/documentation-shell-integration.html][Shell integration]] for iTerm2 on macOS
- [[https://getbitbar.com/][BitBar]] adds program output to menus on macOS