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 $ (or prodigy-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.