Preface
It's been quite a long time since I made another post. I have been quite busy with making an app and making a presentation for my functional programming group which really ate my attention and creativity. So for now, let's see what I can come up with a smaller piece than what I have done before.
Problem
If you have been using prodigy.el, you might get curious on the output buffer of your services. Assuming you have started the service, there are two easy ways to check the process buffer out
switch-to-buffer
The default way to find any buffer. Whether you use helm or ivy or some buffer management technique, you filter out the name of the process buffer and go.prodigy
The canonical way of finding the process buffers. Once in this menu buffer, you can search for your process buffer and hit$
(orprodigy-display-process
)
But nothing beats a key binding in both scenarios(, unless you can't memorize key bindings.) So what we want is when we create a service, we want a key binding associated to opening it's process buffer. Easy right?
I filed an issue for it but I doubt it would be opened so here I am telling you about it. If you don't want a boring story as usual, here is the code. Otherwise, let's get down with the short details.
Decorating The Constructor
So in making services, how do we make one? The function is
prodigy-define-service
. Let's define one.
(prodigy-define-service :name "server" :command "jekyll" :args '("serve") :cwd "~/Fakespace/fnlog.io") (prodigy-start-service (prodigy-find-service "server"))
At the time of writing, this is my minimal jekyll prodigy service to serve my blog and to automatically start when it is defined which is another feature that I desire. Okay, so how do we go about customizing the constructor? There are two ways again.
Make a custom function constructor
This is a safe approach and probably one should lead toward.
(defun fn/prodigy-define-service (&rest args) "A wrapper for `prodigy-define-service' with automatic bindings." (let ((service (apply #'prodigy-define-service args))) (prog1 service ;; Here is where we strike )))
Nothing wrong with this approach. But if you're like me, you sense this is more function decorating than anything else; then you will tend to the other way.
Advice the constructor
Using the benevolent
advice-add
construct.(defun fn/prodigy-define-service-bind-hook (orig-fun &rest args) "When creating a service, check for a :bind keyword to create an automatic keybinding for it." (let ((result (apply orig-fun args))) (prog1 result ;; Attack here again ))) (advice-add #'prodigy-define-service :around #'fn/prodigy-define-service-bind-hook)
Slightly more verbose and a little different, we are able to avoid cluttering constructors with
advice-add
. But you should avoid advising functions if you can, it messes up the contract with other libraries that depend on it and might make things brittle. But when you do use it, you should consider asking the author for the advised feature and see if you can work things out.By saying that, I should have settled on the first approach and not complicate things and probably mean more and easier to comprehend that there is a customization on the constructor. I might refactor my code after making this post. Why did I bother mentioning it? Oh well.
Regardless of hindsight, let's take the second approach since it is the decorating behavior we want here. The more important thing is now how do we create the keybinding?
Keybinding
So the canonical way to go about making a key binding is.
(define-key keymap key command) (global-set-key key command) ;; Assuming keymap is global-map (define-key global-map (kbd "C-c c c c c") #'garbage-collect)
So we need three things to make a key binding: the keymap, the key
and the command. The command is obviously in our case the prodigy
function which opens the service process buffer. Some digging will
reveal that it is prodigy-switch-to-process-buffer
which takes a
service object, one can confirm it with the following snippet.
(prodigy-switch-to-process-buffer (prodigy-find-service "server"))
That was quick, now we have the switch-to-buffer
function for
prodigy. Let's wrap it for our own use.
(defun fn/prodigy-switch-to-process-buffer (service) "Just an wrapper for said function with SERVICE." (prodigy-switch-to-process-buffer service))
So we now have the third part of the recipe, how do we get the others? Rather, how do we define the key binding? What we is to include some extra property or option to the constructor, ideally we want something like this.
(prodigy-define-service :name "server" :command "jekyll" :args '("serve") :cwd "~/Fakespace/fnlog.io" ;; Custom property :bind-map global-map :bind (kbd "C-c c s") )
Staying true to the constructor, let's define :bind-map
and :bind
properties in the keyword constructor where it defines map
and
key
, respectively. Two problems might occur if we pass in extra
attributes in a constructor: it might throw an error because it can't
dispatch the keyword or drop the superfluous keywords which in both
cases implies we have to get the keyword values out before it is
passed in the constructor.
Thankfully this is not the case, running the snippet above yields the following.
((:name "server" :command "jekyll" :args ("serve") :cwd "~/Fakespace/fnlog.io" :bind-map "... output omitted ..." :bind "... output omitted ..."))
It basically returns a list of all the current services where the first one is the one we defined. Looking at the data, it is a property list where our new keywords our retained. Assuming that let's continue with our attack plan.
(let ((result (apply orig-fun args))) (prog1 result ;; Actual attack (lexical-let* ((service (car result)) (name (plist-get service :name)) ;; Just for logging (bind (plist-get service :bind)) (bind-map (or (plist-get service :bind-map) global-map)) ;; Default bind-map to the global keymap ) (when bind (message "Creating binding for %s" name) ;; Logging (define-key bind-map bind (lambda () (interactive) ;; This is needed since it is a command (fn/prodigy-switch-to-process-buffer service)))))))
With this implementation we are done. How quick!? So what we did here
is just extract the relevant pieces we need and just plug it in if
bind
is filled in. I guess we're done right?
Cleanliness
There is one more enhancement is we can do is to name the view
function. Since we are defining an anonymous command, we can't reuse
the command unless you are in favor of command-execute-key
. And if
you use which-key
and whenever the command is displayed or queried,
it just says lambda
or something unhelpful. So this optional section
is primarily just for that. So let's refactor the anonymous command.
First, how do we define the command name? We can either ask for it via
:bind-command-name
keyword or generate it ourselves. We can create
a quick and safe symbol with gensym
like so.
(gensym "symbol-prefix") ;; symbol-prefix800
We can use that to create a symbol given a prefix. Which in turn is a
good idea, to give our command a prefix or namespace. As for me, I use
the prefix fn/
. So let's put an option to define our namespace.
(defvar fn/prodigy-command-name-prefix "fn/" "The prefix when creating binding prodigy view commands.")
Ideally, our command name is prefix plus the bind command name. Which is easy to work with.
(lexical-let* ((service-name (plist-get service :name)) (command-name (or (plist-get service :bind-command-name) (symbol-name (gensym "prodigy-view-")))) (function-symbol (intern (format "%s%s" fn/prodigy-command-name-prefix command-name))) (service service)) ;; How do we create our named command? )
Lastly, we need to create our function. If you're thinking we should
use defun
with an interactive
spec, then it is slightly more
complicated than just using fset
with an anonymous command by a
backtick.
(fset my-interned-function-symbol `(lambda () ,(format "A prodigy view function for %s" service-name) (interactive) (fn/prodigy-switch-to-process-buffer (quote ,service))))
This is a nice template of wrapping a function into a command. When I
was thinking about it, I knew fset
is the go to function when you
want it to be found or discovered aside from defun
; the other thing
I found some difficulty is using backtick. If you didn't use a
backtick, you can't add the documentation string which is a minor
detail or did I just complicate myself again? Oh well.
With that we can wrap it up in a neat bow.
(defun fn/prodigy-prepared-switch-to-process-buffer (service) "Another wrapper to make specific functions for viewing SERVICE." (lexical-let* ((service-name (plist-get service :name)) (command-name (or (plist-get service :bind-command-name) (symbol-name (gensym "prodigy-view-")))) (prefix "fmpv/") (function-symbol (intern (format "%s%s" prefix command-name))) (service service)) (fset function-symbol `(lambda () ,(format "A prodigy view function for %s" service-name) (interactive) (fn/prodigy-switch-to-process-buffer (quote ,service)))) function-symbol))
Going back to our original function.
(defun fn/prodigy-define-service-bind-hook (define-service &rest args) "When creating a service, check for a :bind keyword to create an automatic keybinding for it." (let ((result (apply define-service args))) (prog1 result (let* ((service (car result)) (name (plist-get service :name)) (bind (plist-get service :bind)) (bind-map (or (plist-get service :bind-map) global-map))) (when bind (message "Creating binding for %s" name) (define-key bind-map bind (fn/prodigy-prepared-switch-to-process-buffer service))))))) (advice-add #'prodigy-define-service :around #'fn/prodigy-define-service-bind-hook)
And that's pretty much it and in action.
(prodigy-define-service :name "server" :command "jekyll" :args '("serve") :cwd "~/Fakespace/fnlog.io" ;; Custom property :bind-command-name "server-jekyll" :bind-map global-map :bind (kbd "C-c c s") )
This creates the command fn/server-jekyll
which is bound to C-c c
s
. Success!
Closing Words
For now I am pretty happy with the implementation, I can jump to any prodigy service I defined quickly. There is one thing one can ask from the author is that how the prodigy buffer is displayed. Like with this feature, the process buffer appears in the other window. Not a big deal.
And with this, I may have no reason to visit the prodigy buffer itself aside from starting services up. If you wanted to go one up, you could automatically start a service you visit, which you can decorate the process switch function. The other feature I came up along with workgroups2 is to set the default filter per workgroup. For example, I have five workgroups and each one has a prodigy service tightly tied with it, I don't need to see the other services in the prodigy buffer since it does not relate to the workgroup.
I wonder what other prodigy features can be made possible.