/emacs-run-command

Efficient and ergonomic external command invocation via Helm or Ivy.

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

MELPA

run-command

Emacs, the text editor where you can read mail and play Tetris, is often cast in opposition to the Unix philosophy of "do one thing well" and "combine programs". It's a false dichotomy. Emacs can do a lot on its own and can be combined usefully with other programs.

run-command makes the combination convenient through a simple configuration format and an interaction flow that stays out of your way. Where you'd usually reach for a shell or look for a specialized major mode, with run-command you can write a short recipe and get a command that is easy to bring up, invoke, and keep track of, without leaving Emacs.

Table of Contents

Demo

The screencast below shows using run-command to 1) clone a project from a boilerplate, 2) execute a file on every save, and 3) start the test runner.

demo

Features

  • Unopinionated: run a command against a word, a file, a project, a service, anything.
  • Minimal cognitive tax: one key binding, one configuration variable.
  • Flexible configuration: hardcode commands, generate them dynamically based on context, or anything in between.

Installation

Available from MELPA.

Quickstart

  1. Add a "command recipe" to your init file, for example:
(defun run-command-recipe-example ()
  (list
   ;; Arbitrary command
   (list :command-name "say-hello"
         :command-line "echo Hello, World!")

   ;; Do something with current directory
   (list :command-name "serve-http-dir"
         :command-line "python3 -m http.server 8000"
         :display "Serve current directory over HTTP")

   ;; Do something with current file if it's a README
   ;; (uses https://github.com/joeyespo/grip)
   (when (equal (buffer-name) "README.md")
     (list :command-name "preview-github-readme"
           :command-line "grip --browser --norefresh"
           :display "Preview GitHub README"))

   ;; Do something with current file if it's executable
   (when-let* ((buffer-file (buffer-file-name))
               (executable-p (and buffer-file (file-executable-p buffer-file))))
     (list :command-name "run-buffer-file"
           :command-line buffer-file
           :display "Run this buffer's file"))

   ;; Do something with current word
   ;; (uses https://wordnet.princeton.edu/documentation/wn1wn)
   (when-let ((word (thing-at-point 'word t)))
     (list :command-name "wordnet-synonyms"
           :command-line (format "wn '%s' -synsn -synsv -synsa -synsr" word)
           :display (format "Look up '%s' synonyms in wordnet" word)))))
  1. Customize run-command-recipes and add run-command-recipe-example to the list.

  2. Type M-x run-command RET.

Read more about invocation, configuration, and how to add commands, or check out some recipe examples.

Invocation

Type M-x run-command or bind run-command to a key:

(global-set-key (kbd "C-c c") 'run-command)

Or:

(use-package run-command
  :bind ("C-c c" . run-command)

When completing via Helm or Ivy, you can edit a command before running it by typing C-u RET instead of RET.

Configuration

Write recipe functions into your init file as described in the quickstart and the tutorial, then add them via M-x customize to the run-command-recipes variable. This is the only required configuration.

By default, commands are run in compilation-mode. See Lightweight external command integration in Emacs via compilation mode for some notes on how to make the most of compilation-mode. An alternative method (and probably future default) is term-mode plus compilation-minor-mode, especially useful for commands with rich output such as colors, progress bars, and screen refreshes, while preserving compilation-mode functionality. Set run-command-run-method to term and please comment on issue #2 if you find issues.

The auto-completion framework is automatically detected. It can be set manually by customizing run-command-completion-method.

Tutorial: adding commands

Readable command names

To provide a more user-friendly name for a command, use the :display property:

(defun run-command-recipe-example ()
  (list
   (list :command-name "serve-http-dir"
         :command-line "python3 -m http.server 8000"
         :display "Serve directory over HTTP port 8000")))

Specifying the working directory

A command runs by default in the current buffer's directory. You can make it run in a different directory by setting :working-dir.

For example, you want to serve the current directory via HTTP, unless you're visiting a file that is somewhere below a public_html directory, in which case you want to serve public_html instead:

(defun run-command-recipe-example ()
  (list
   (list :command-name "serve-http-dir"
         :command-line "python3 -m http.server 8000"
         :display "Serve directory over HTTP port 8000"
         :working-dir (let ((project-dir
                             (locate-dominating-file default-directory "public_html")))
                        (if project-dir
                            (concat project-dir "public_html")
                          default-directory)))))

See the Hugo project recipe for a recipe that uses the project's directory for all commands.

Enabling and disabling depending on context

To disable a command in certain circumstances, return nil in its place.

For example, you want to enable a command only when the current buffer is visiting an executable file:

(defun run-command-recipe-example ()
  (let ((buffer-file (buffer-file-name)))
    (list
     (when (and buffer-file (file-executable-p buffer-file))
       (list
        :command-name "run-buffer-file"
        :command-line buffer-file
        :display "Run this buffer's file")))))

See the executable file recipe for a variant that also re-runs the file on each save.

See the Hugo project recipe for a recipe that switches off entirely when you're not in a Hugo project.

Choosing a recipe name

You can name a recipe function anything. If the name begins with run-command-recipe-, that will be removed when displaying commands.

For example:

(defun run-command-recipe-example ()) ;; displays as "example"
(defun my-command-recipe ())          ;; displays as "my-command-recipe"

Generating commands on the fly

Recipes are plain old Lisp functions, so they generate commands based on e.g. project setup.

See the NPM project recipe, which uses a JavaScript's project package.json file to generate commands, and the Make project recipe, which does the same for Makefile projects.