Problem
The Elisp library prodigy.el helps you manage your external processes;
in particular, it allows you to listen to the process log which allows
you to execute commands on certain states. Although we have
start-process
and set-process-filter
, this library wraps those
lower level constructs into a more manageable object with hooks and
properties.
One common feature when using processes is to be notified when it is
ready or something happens. For example, I use offlineimap with gnus
to get my mail where I want to be notified when there is one or jekyll
to make this blog where I want to told when the blog is built
successfully or not. I could use the simple message
construct but to
be more flexible I use the library alert.el.
That notifying library features more than just using message
, it can
use libnotify
, growl
and more importantly the fringe
. A fringe
alert uses set-face-background
to change the color of the buffer
sides and removes it on action. You can try this one liner to
understand better:
(set-face-background 'fringe "red")
So when there is a process event, I want a fringe alert. I have no
snippet to give here only an outline on how I went about it. For this
post, I will use jekyll
as an example.
Using prodigy
To define a jekyll
process with prodigy
, here is how you will go
about it:
(prodigy-define-service :name "Gnus-Offlineimap" ;; Service name or id :cwd "~/Fakespace/fnlog.io" ;; Working directory when the service is called :command "jekyll" ;; The jekyll command :args (list ;; Command arguments "serve" "--drafts" "--ports" (number-to-string 34000)) :port 34000 ;; Optional port property :kill-signal 'sigkill ;; Kill signal to send when closing :on-output #'ignore ;; The output listener we are interested in )
This is more or less how I setup my blog process service but I use the
tag system to make it a more reusable. To emphasize convenience, I
would use this library instead of handling it with start-process
,
with-temp-buffer
and default-directory
. To get a better picture,
you can see the new service with a special tabulated and process
buffer.
What we are interested in with the logs are two lines that contain the string/text:
Server running
to indicate the blog isready
...done in
to indicate the blog isbuilt
and should be refreshed
The :on-output
allows us to tap into this which takes a function
with a plist argument that has :service
and :output
properties. In
our context of listenting to the following lines:
(defun fn/prodigy-jekyll-state-change-listener (&rest args) ;;defun* can also work (lexical-let* ((output (plist-get args :output)) ;; Get the arguments (service (plist-get args :service))) (pcase output ((pred (string-match-p "Server running")) (prodigy-set-status service 'ready) ;; Manually set the status ;; Trigger ready alert ) ((pred (string-match-p "Error:")) ;; Trigger error alert ) ((pred (string-match-p "...done")) ;; Trigger done built alert ) (_ ;; Do nothing otherwise nil)))) ;; Redefining the service (prodigy-define-service ;; Previous definition :on-output 'fn/prodigy-jekyll-state-change-listener )
If you don't have string-match-p
since you are using an earlier
version, it can be shivved like so:
(unless (fboundp 'string-match-p) (defsubst string-match-p (regexp string &optional start) "\ Same as `string-match' except this function does not change the match data. Taken from `subr-x'" (let ((inhibit-changing-match-data t)) (string-match regexp string start))))
Aside from using pcase
, it is pretty much just listening to the
output. As an aside, listening to the output is pretty much determined
by the process itself; so some awkward log state management might be
present for other services. For example, there might be a start line,
then followed by either an error or process line, finally an end line;
in that scenario, one has to keep track if there is an error or not to
determine at the end line if there is an error instead of notifying at
each one. Logging and state machines, some nuances when dealing with
listening to logs.
We move on to using alert
.
Using alert
Using alert
is pretty straightforward, the first argument is the
message and the rest are some options. I encourage you to check it out
but for our case, we will be focused on using the fringe type:
(alert "Hello Alert") ;; Default alert with message (alert "Hello Fringe Alert" ;; Using a fringe alert :severity 'trivial :style 'fringe)
If you use this snippet, you will get a purple fringe alert which
comes from alert-severity-colors
.
((urgent . "red") (high . "orange") (moderate . "yellow") (normal . "green") (low . "blue") (trivial . "purple"))
Weirdly, the severity handles the color. Either way, there isn't many
colors to work with, so if you want to add one like Rebecca Purple
or Lavender
. You have to add to do the following:
(add-to-list 'alert-log-severity-functions (cons 'lavender #'alert--log-trace)) (add-to-list 'alert-severity-colors (cons 'lavender "#b378d3")) (alert "My lavender alert" :style 'fringe :severity 'lavender)
This is not so bad but when you have several services that uses fringe alerts or have to create one, you might want a function that does this. Here is my version of that function after looking at the source:
(require 'cl-lib) (defun* fn/alert-color (message &rest args &key color &allow-other-keys) "A custom alert that focuses on defining a fringe with COLOR key with a hex value." ;; Step 01: Create hex color symbol (lexical-let* ((hex-color (replace-regexp-in-string "#" "" color)) (hex-symbol-name ;; Prepend alert-color-- to the hex code (format "alert-color--%s" hex-color)) (hex-symbol ;; Check if the symbol exists or default (or (intern-soft hex-symbol-name) (intern hex-symbol-name)))) ;; Step 02: Add the symbol pair to the lists (unless (cdr (assoc hex-symbol alert-log-severity-functions)) (add-to-list 'alert-log-severity-functions (cons hex-symbol #'alert--log-trace))) (unless (cdr (assoc hex-symbol alert-severity-colors)) (add-to-list 'alert-severity-colors (cons hex-symbol color))) ;; Step 03: Handle an extra :color property that ;; defaults :style and :severity (lexical-let ((color-properties (list :style 'fringe :severity hex-symbol)) (colorless-properties (cl-reduce ;; Just removes :color from the property list (lambda (val props) (if (equal :color val) (cdr props) (cons val props))) args :from-end t :initial-value (list)))) (apply #'alert (append (list message) color-properties colorless-properties)))))
Although a little long, this encapsulates the symbol creation, adding
to the lists and handling a :color
property. It does not handle more
fine grained severity options and it might not property decorate
around alert
but it does the job. You can see it in action here:
(fn/alert-color "Here is my colored alert" :color "#123456")
In the interest of being canonical with alert
, I would have defined
a custom style but I could not get a custom property or argument like
:color
to be used; I could have reused :severity
but it doesn't
fit the context. So I had to create a decorator for a custom color
property despite the ease of doing the following:
;; Copied from alert-fringe- (defun fn/alert-flash-notify (info) (set-face-background 'fringe (plist-get info :color))) (defun fn/alert-flash-restore (info) (copy-face 'alert-saved-fringe-face 'fringe)) (alert-define-style 'fn/flash :title "Change the fringe color" :notifier #'fn/alert-flash-notify :remover #'fn/alert-flash-restore) ;; If it did work... (alert "Here is a simpler alert." :style 'fn/flash :color "#123456")
So now we can get a colored fringe alert without handling the symbols and lists.
Combining alert
and prodigy
Now that we have the two we can easily merge them in the following snippet:
(defun fn/prodigy-jekyll-state-change-listener (&rest args) ;;defun* can also work (lexical-let* ((output (plist-get args :output)) ;; Get the arguments (service (plist-get args :service))) (pcase output ((pred (string-match-p "Server running")) (prodigy-set-status service 'ready) ;; Manually set the status (fn/alert-color "Blog is up and running" :color "#01dddd")) ((pred (string-match-p "Error:")) (fn/alert-color "Blog has a build error. Check out the logs." :color "#fd8a5e")) ((pred (string-match-p "...done")) (fn/alert-color "Blog has successfully been built." :color "#e0e300")) (_ ;; Do nothing otherwise nil))))
With that, everything should be in order; however, what I did instead
is created a hook so that other actions aside might be taken based on
its state. As an example, I check the blog's output using w3m
and
when the blog is built, I want to reload all the web buffers
pertaining to my blog. This snippet does it:
(defun fn/prodigy-jekyll-state-w3m-reload (service state) (lexical-let ((service-name (plist-get service :name))) (pcase state ;; My hook takes a service and state argument ('built (save-window-excursion (mapc (lambda (w3m-buffer) (ignore-errors (lexical-let* ((url (with-current-buffer w3m-buffer w3m-current-url)) (parts (w3m-parse-http-url url)) (host (elt parts 1)) (port (elt parts 2))) (when (and (= port 34000) (string= host "127.0.0.1")) (with-current-buffer w3m-buffer (w3m-reload-this-page)))))) (w3m-list-buffers)))) (_ nil)))) ;; The hook I created, modified for this post (add-hook 'fn/prodigy-jekyll-state-change-hook #'fn/prodigy-jekyll-state-w3m-reload)
That's the thing about :on-output
, you can't add to it like a hook
so you have to make one. Using the output as a hook, I have an alert
and auto-reload mechanism.
Lastly, if you forgot the meaning of the color, it would be nice if there was also a message to back it up. By default, using this style of the alert produces no minibuffer message to appear which is appropriate. However, I would like to be reminded what it means with this snippet:
(defun fn/alert-fringe-notify-message (info) "Log `fringe' style with `message'. This is to support `fn/alert-color' if the color flash needs a reminder." (message (plist-get info :message))) (advice-add 'alert-fringe-notify :after #'fn/alert-fringe-notify-message)
Or you could add that to fn/alert-color
if advicing is overkill.
Conclusion
Now that we have candied alerts for our services, what else can be done with these libraries?
- Service dependencies
- Log alerts to file
If more services such as react-native
come and go, we can now easily
creates alerts for it.