Fetching contributors…
Cannot retrieve contributors at this time
236 lines (194 sloc) 8.51 KB
;;; firestarter.el --- Execute (shell) commands on save
;; Copyright (C) 2015 Vasilij Schneidermann <>
;; Author: Vasilij Schneidermann <>
;; URL:
;; Version: 0.2.5
;; Keywords: convenience
;; This file is NOT part of GNU Emacs.
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; This global minor mode allows you to run (shell) commands on save.
;; See the README for more info:
;;; Code:
(require 'format-spec)
(require 'ansi-color)
(defgroup firestarter nil
"Execute shell commands on save."
:group 'convenience
:prefix "firestarter-")
(defvar firestarter nil
"Command to run on file save.
A string value is interpreted as shell command and passed to an
asynchronous subprocess. A symbol value is interpreted as
command and executed interactively. A list value is interpreted
as code and evaluated.")
(make-variable-buffer-local 'firestarter)
(defcustom firestarter-default-type 'silent
"Default shell command reporting type.
It may be one of the following values:
nil, 'silent: Don't report anything at all.
'success: Report on successful execution (return code equals zero).
'failure: Report on failed execution (return code equals non-zero).
t, 'finished: Report after either outcome once the subprocess quit."
:type '(choice (const :tag "Silent" silent)
(const :tag "Success" success)
(const :tag "Failure" failure)
(const :tag "Finished" finished))
:group 'firestarter)
(defvar firestarter-type nil
"Current shell command reporting type.
See `firestarter-default-type' for valid values.")
(make-variable-buffer-local 'firestarter-type)
(defvar firestarter-process nil
"Process associated with current buffer.")
(make-variable-buffer-local 'firestarter-process)
(defcustom firestarter-buffer-name "*firestarter*"
"Buffer name of the reporting buffer for shell commands."
:type 'string
:group 'firestarter)
(defcustom firestarter-reporting-functions nil
"Abnormal hook run after process termination.
The process is used as argument. See
`firestarter-report-to-buffer' for the default value and as
example for writing your own reporting function."
:type 'hook
:group 'firestarter)
(add-hook 'firestarter-reporting-functions 'firestarter-report-to-buffer)
(defcustom firestarter-reporting-format
(concat (propertize "%b (%c):" 'face 'highlight)
(propertize "---" 'face 'shadow)
"Format string for a single report item.
Available format codes are:
%b: Buffer name
%c: Return code
%s: Process output"
:type 'string
:group 'firestarter)
(defun firestarter-command (command &optional type)
"Execute COMMAND in a shell.
Optionally, override the reporting type as documented in
`firestarter-default-type' with TYPE."
(if (and firestarter-process
(not (memq (process-status firestarter-process)
'(exit signal nil))))
(error "Process already running")
(setq firestarter-process
(start-process "firestarter" nil
shell-file-name shell-command-switch
(firestarter-format command)))
(process-put firestarter-process 'output "")
(process-put firestarter-process 'type
(or type firestarter-type firestarter-default-type))
(process-put firestarter-process 'buffer-name
(buffer-name (current-buffer)))
(set-process-filter firestarter-process 'firestarter-filter)
(set-process-sentinel firestarter-process 'firestarter-sentinel)))
(defun firestarter-format (string)
"Apply format codes on STRING.
Available format codes are:
%b: Buffer name. Equals the file name for buffers linked with
files. Beware that this is merely convention and buffers can be
renamed to conform to their unique name constraint!
%p: Full path of the file associated with the buffer. Decomposes
into a directory and file name part. If there is no file
association, the value is an empty string. As the following
format codes are directly derived from this value, the same
caveat applies to them as well.
%d: Directory name of the file associated with the buffer.
Equals the full path without the file name.
%f: File name of the file associated with the buffer. Decomposes
into a file stem and a file extension.
%s: File stem of the file associated with the buffer. Equals the
file name without its extension.
%e: File extension of the file associated with the buffer.
Equals the file name without its stem. Includes the period if
an extension is present, otherwise the value is an empty
(let* ((buffer (shell-quote-argument (buffer-name)))
(path (shell-quote-argument (or (buffer-file-name) "")))
(directory (shell-quote-argument (file-name-directory (or path ""))))
(file (shell-quote-argument (file-name-nondirectory (or path ""))))
(stem (shell-quote-argument (file-name-sans-extension file)))
(extension (shell-quote-argument (file-name-extension file t))))
(format-spec string (format-spec-make ?b buffer ?p path ?d directory
?f file ?s stem ?e extension))))
(defun firestarter-filter (process output)
"Special process filter.
Appends OUTPUT to the process output property."
(process-put process 'output (concat (process-get process 'output) output)))
(defun firestarter-sentinel (process _type)
"Special process sentinel.
It retrieves the status of PROCESS, then sets up and displays the
reporting buffer according to the reporting type."
(when (memq (process-status process) '(exit signal nil))
(run-hook-with-args 'firestarter-reporting-functions process)))
(defun firestarter-report-to-buffer (process)
"Sets up and displays a reporting buffer.
Process output, buffer name, return code and reporting type are
all derived from PROCESS. See also `firestarter-default-type'."
(let ((return-code (process-exit-status process))
(buffer-name (process-get process 'buffer-name))
(output (process-get process 'output))
(type (process-get process 'type))
(unless (memq type '(silent nil))
(with-current-buffer (get-buffer-create firestarter-buffer-name)
(let ((inhibit-read-only t))
(goto-char (point-max))
(format-spec firestarter-reporting-format
(format-spec-make ?b buffer-name
?c return-code
?s output))))
(setq end (point-max))))
(when (or (and (eq type 'success) (= return-code 0))
(and (eq type 'failure) (/= return-code 0))
(memq type '(finished t)))
(let ((window (display-buffer firestarter-buffer-name)))
(when window
(set-window-point window end)))))))
(defun firestarter ()
"Hook function run after save.
It dispatches upon the value type of `firestarter'."
(when firestarter
((stringp firestarter)
(firestarter-command firestarter))
((functionp firestarter)
(call-interactively firestarter))
((listp firestarter)
(eval firestarter))
(t (error "Invalid value for `firestarter': %s" firestarter)))))
(defun firestarter-abort ()
"Abort the currently active firestarter process."
(when firestarter-process
(delete-process firestarter-process)))
(define-minor-mode firestarter-mode
"Toggle `firestarter-mode'.
When activated, run a command as specified in the buffer-local
`firestarter' variable on every file save."
:global t
(if firestarter-mode
(add-hook 'after-save-hook 'firestarter)
(remove-hook 'after-save-hook 'firestarter)))
(provide 'firestarter)
;;; firestarter.el ends here