Writing Recipes
Choosing the name for a recipe
If the name begins with run-command-recipe-
, the prefix is removed when displaying commands, otherwise the entire function name is used:
(defun run-command-recipe-boilerplates ()) ;; shows up as "boilerplates"
(defun my-command-recipe ()) ;; shows up as "my-command-recipe"
Readable command names
You can provide a user-friendly name via the :display
property:
(defun run-command-recipe-example ()
(list
(list :display "Serve current directory over HTTP port 8000"
:command-name "serve-http-dir"
:command-line "python3 -m http.server 8000")))
Context-independent commands
The simplest command hardcodes everything:
(defun run-command-recipe-example ()
(list
(list :command-name "create-webdev-project"
:command-line "git clone https://github.com/h5bp/html5-boilerplate webdev-project"
:display "Create a web project from HTML5 boilerplate")))
Context-sensitive commands
A recipe is a plain function, so you can evaluate Lisp code to query the context. For example, to find out the current buffer’s file name and pass it to the command:
(defun run-command-recipe-example ()
(list
(when-let ((buffer-file (buffer-file-name)))
(list :display "Count lines of code"
:command-name "count-lines-of-code"
:command-line (format "sloccount '%s'" buffer-file)))))
(Note that we use when-let
to guard against buffers that aren’t associated with a file, such as *scratch*
.)
To find out the word at point and pass it to the command:
(defun run-command-recipe-example ()
(list
(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 for '%s' synonyms in wordnet" word)))))
Specifying a different working directory
By default, commands run in the same directory as the buffer from where run-command
is launched. You can specify a different one via :working-dir
.
For example, to run a command in the base directory of a project managed with Makefile
:
(defun run-command-recipe-example ()
(list
(when-let ((project-dir (locate-dominating-file default-directory "Makefile")))
(list :display "Run make with default target"
:command-name "make-default"
:command-line "make"
:working-dir project-dir))))
Toggling individual commands on or off depending on context
To disable a command, return nil
in its place. when
and when-let
are convenient ways of doing so.
For example, to only enable a command when the buffer’s file is executable:
(defun run-command-recipe-example ()
(list
(let ((buffer-file (buffer-file-name)))
(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")))))
Requesting user input before executing command
The :command-line
field accepts a string or a function that returns a string. You can use the latter to dynamically change the command line based on user input:
(defun run-command-recipe-example ()
(list
(list
:command-name "whois"
:command-line (lambda ()
(format "whois %s" (read-string "Enter domain name: ")))
:display "whois")))
Toggling entire recipes on or off depending on context
To disable an entire list, return nil
in its place. As for individual commands, when
and when-let
are convenient ways of doing so.
For example, to disable the entire recipe when we’re not in a JavaScript project:
(defun run-command-recipe-eldev ()
(when-let ((project-dir (locate-dominating-file default-directory "package.json")))
(list
(list :command-name "test:watch"
:command-line "npm run test:watch"
:display "test:watch"
:working-dir project-dir))))
(Contrast the position of when-let
with the previous examples.)
Launching long-lived commands that re-run on file changes
Utilities such as watchexec (opens in a new tab) make it easy to convert a one-off command into a watch-style utility.
For example, to regenerate a PDF whenever you save a file in Markdown or another pandoc-supported format:
(defun run-command-recipe-example ()
(list
(when-let ((buffer-file (buffer-file-name)))
(list :display "Generate PDF"
:command-name "generate-pdf"
:command-line (format "watchexec --clear --watch '%s' 'pandoc --standalone -t html5 -o /tmp/preview.pdf \'%s\''"
buffer-file
buffer-file)))))
Generating a recipe from project contents
Entire recipes can be generated dynamically based on a project’s contents.
For examples of generation from project management file, see the package.json recipe (opens in a new tab) or the Makefile example (opens in a new tab).
Likewise one may use the contents of a directory:
(defun run-command-recipe-project-scripts ()
(when-let ((file-accessible-directory-p "scripts"))
(mapcar (lambda (script-name)
(let ((file (expand-file-name script-name "scripts")))
(when (and (file-regular-p file)
(file-executable-p file))
(list :command-line file
:command-name script-name))))
(directory-files "scripts"))))
Overriding the default runner
Normally, the variable run-command-default-runner
determines how commands are launched (e.g. whether in term-mode
, compilation-mode
, etc).
The default runner can be overridden on a per-command basis using the :runner
property.
You might want to use this if a particular runner glitches on a command’s output.
For example:
(defun run-command-recipe-sysadmin ()
(list
(list :command-name "htop"
:command-line "htop"
:runner 'run-command-runner-vterm)))
Hooks
To execute Lisp code just after the command has been launched, assign a function to the :hook
property, either in the form of a symbol naming a function, or as a lambda.
For example, to enable compilation-minor-mode
:
(defun run-command-recipe-example ()
(when-let* ((project-dir
(locate-dominating-file default-directory "Eldev")))
(list (list
:command-name "lint"
:command-line "eldev lint"
:display "lint"
:runner 'run-command-runner-term
:hook 'compilation-minor-mode
:working-dir project-dir))))