UP | HOME
mattduck
PostsEmailRSSGithubCVEmacs.London  

2020-Feb-25

Emacs as a fuzzy launcher and Alfred-replacement

When I was using MacOS, I used to like Alfred, the multi-purpose fuzzy finder/program-launcher. If you haven't used it, it's similar nowadays to Spotlight, which is built-in and accessed by pressing cmd+space.

On Linux, the closest thing I've used to Alfred is Albert, which has a lot of the same functionality. It's a good project, and I was happy with it for a while.

Sometime last year though I read a great post by Álvaro Ramírez, demonstrating a proof of concept for building a similar interface in Emacs, using Ivy and Hammerspoon (on MacOS).

For six months or so I've been using my own version of this to launch programs, run system commands and perform searches:

Emacs Alfred gif

1. How?

Most of the credit goes to Álvaro. I just adapted his frame-managing code to work with Helm and i3, and wrote some Helm sources to implement the features that I want.

2. Tell me more

Helm is a popular fuzzy completion framework for Emacs. I use it for many things already (selecting buffers, M-x commands, help commands, etc.), so it's the natural choice for me to implement any kind of fuzzy matching feature.

The entry point is the md/alfred function below, which does a few things:

  1. Create a buffer named *alfred*.
  2. Make a new "frame" (ie. a new X window) for this buffer, applying some parameters to resize and bring it into focus.
  3. Set variables to disable the mode-line and the message area, and apply some Helm styling parameters.
  4. Call Helm with a list of custom "sources" (which we'll get to next), telling it to use our new buffer.
  5. After Helm is done, delete the new frame and kill our *alfred* buffer.
(defun md/alfred ()
  (interactive)
  (with-current-buffer (get-buffer-create "*alfred*")
    (let ((frame (make-frame '((name . "alfred")
                               (window-system . x)
                               (auto-raise . t) ; focus on this frame
                               (height . 10)
                               (internal-border-width . 20)
                               (left . 0.33)
                               (left-fringe . 0)
                               (line-spacing . 3)
                               (menu-bar-lines . 0)
                               (right-fringe . 0)
                               (tool-bar-lines . 0)
                               (top . 48)
                               (undecorated . nil) ; enable to remove frame border
                               (unsplittable . t)
                               (vertical-scroll-bars . nil)
                               (width . 110))))
          (alert-hide-all-notifications t)
          (inhibit-message t)
          (mode-line-format nil)
          (helm-mode-line-string nil)
          (helm-full-frame t)
          (helm-display-header-line nil)
          (helm-use-undecorated-frame-option nil))
      (helm :sources (list (md/alfred-source-system)
                           (md/alfred-source-apps)
                           (md/alfred-source-search))
            :prompt ""
            :buffer "*alfred*")
      (delete-frame frame)
      ;; If we don't kill the buffer it messes up future state.
      (kill-buffer "*alfred*")
      ;; I don't want this to cause the main frame to flash
      (x-urgency-hint (selected-frame) nil))))

3. Helm sources

The code above provides a pop-up window that runs Helm, but that's it. To implement useful functionality, we need to write some Helm "sources", which control the input and output integrations with Helm.

4. System commands: lock, sleep, restart, shutdown

Emacs Alfred system commands gif

Sleep, restart and shutdown are all features of systemctl. Lock features are also accessible via the command line. This means we just have to build a Helm source that runs an external command when we enter a particular word.

There are two parameters to helm-build-sync-source that make this easy: :candidates and :action.

(defun md/alfred-source-system ()
  (helm-build-sync-source "System"
    :multimatch nil
    :requires-pattern nil
    :candidates '(("Lock" . "xset dpms force off")  ;; turns laptop screen off and triggers i3lock
                  ("Sleep" . "systemctl suspend -i")
                  ("Restart" . "systemctl reboot -i")
                  ("Shutdown" . "systemctl poweroff -i"))
    :action '(("Execute" . (lambda (candidate)
                             (shell-command (concat candidate " >/dev/null 2>&1 & disown") nil nil))))))

Each item in :candidates is an alist where the car (the left side) represents the displayed value in Helm, and the cdr (the right side) represents the value that gets passed to our action.

:action defines a lambda which operates on these right-side values when selected. We just have to use (shell-command) to execute the selected command. We redirect all output to /dev/null so it doesn't display anywhere, and also run disown so that the process is no longer owned by the shell - this will let you close Emacs without affecting any program that you've executed with Helm.

5. Launching programs

Emacs Alfred launching programs gif

This has a similar solution to our system commands implementation, with one extra step: where do we find the list of GUI programs to launch? You could define this manually, but it would be nice if we could automatically retrieve them, Alfred-style.

On Arch Linux, you can find a list of .desktop files installed in /usr/share/applications. These Desktop entries implement the XDG Desktop Menu specification, which tells environments like GNOME and KDE how to launch GUI programs, what name to display in a launcher menu, what icons to use, etc.

In theory, we could parse these files to get the user-friendly name for the program (maybe by using lsdesktop). Instead, I've done something worse but much quicker to implement: we just list all the .desktop files in the directory, and then pass them to gtk-launch to execute them.

As above, just make sure to disown the process, so that it isn't coupled to Emacs:

(defun md/alfred-source-apps ()
  (helm-build-sync-source "Apps"
    :multimatch nil
    :requires-pattern nil
    :candidates (lambda ()
                  (-map
                   (lambda (item)
                     (s-chop-suffix ".desktop" item))
                     (-filter (lambda (d) (not (or (string= d ".") (string= d ".."))))
                              (directory-files "/usr/share/applications"))))
    :action '(("Launch" . (lambda (candidate)
                            (shell-command (concat "gtk-launch " candidate " >/dev/null 2>&1 & disown") nil nil))))))

6. Web search

Emacs Alfred browser search gif

Web search is a bit different, as we're not directly launching programs - we instead need to build a URL with the typed search term.

I also want one more feature from Alfred: key prefixes to trigger particular searches. Typing d my search term should open a search in DuckDuckGo, and typing g my search term should search Google.

So let's again define a :candidates list, with the displayed value as the car and the actual value as the cdr. This time though, our "value" is itself going to be an alist, containing the letter prefix, and the URL structure for that search:

(defvar md/alfred-source-search-candidates
  '(("DuckDuckGo" . ("d" . "https://www.duckduckgo.com/?q=%s"))
    ("Google" . ("g" . "https://www.google.co.uk/search?q=%s"))))

In our previous Helm sources, we would type something, and Helm would match it against the display value of our candidate: eg. if I typed "Loc", it would put me on the entry named "Lock". This time, we're going to use :match to define a custom matching function, which will look up the assigned letter for each candidate.

...
  ;; Count it as a match if the prefix matches, eg. "d ..."
  :match '((lambda (candidate)
             (string= (car (cdr (assoc candidate md/alfred-source-search-candidates)))
...                    (car (split-string helm-pattern)))))

This should work, but we can make it a bit nicer to use: instead of just displaying "DuckDuckGo" as the selected item in Helm, we could display "DuckDuckGo: my current search term". This can be done with :filtered-candidate-transformer, which transforms the displayed value for our currently-narrowed list of candidates:

...
  ;; Instead of displaying the exact thing that you type, display "DuckDuckGo: %s..."
  :filtered-candidate-transformer '((lambda (candidates source)
                                      (map 'list (lambda (c)
                                                   (cons (format "%s: %s" (car c)
                                                                 (md/strip-first-word helm-pattern)) (cdr c)))
                                           candidates)))
...

Finally, we have the :action stage. This is simple: it will just build and encode the URL, and use (browse-url) to open it in our preferred browser.

Overall, it looks like this:

(defvar md/alfred-source-search-candidates
  '(("DuckDuckGo" . ("d" . "https://www.duckduckgo.com/?q=%s"))
    ("Google" . ("g" . "https://www.google.co.uk/search?q=%s"))))

(defun md/strip-first-word (s)
  "Remove the first word from a string"
  (string-remove-prefix (format "%s " (car (split-string s))) s))

(defun md/alfred-source-search ()
  (helm-build-sync-source "Search"
    :nohighlight t
    :nomark t
    :multimatch nil
    :requires-pattern t
    :candidates md/alfred-source-search-candidates
    ;; Count it as a match if the prefix matches, eg. "d ..."
    :match '((lambda (candidate)
               (string= (car (cdr (assoc candidate md/alfred-source-search-candidates)))
                        (car (split-string helm-pattern)))))
    :fuzzy-match nil
    ;; Instead of displaying the exact thing that you type, display "DuckDuckGo: %s..."
    :filtered-candidate-transformer '((lambda (candidates source)
                                        (map 'list (lambda (c)
                                                     (cons (format "%s: %s" (car c)
                                                                   (md/strip-first-word helm-pattern)) (cdr c)))
                                             candidates)))
    ;; Build the URL, replacing %s with your input. Open it with browse-url.
    :action '(("Search" . (lambda (candidate)
                            (browse-url (format (cdr candidate) ;; the url
                                                (url-hexify-string
                                                 ;; This removes the "g " part from the string
                                                 (md/strip-first-word helm-pattern)))))))))

7. Launching (md/alfred) with i3

We now have all the above functionality inside Emacs. It can be launched with (md/alfred). However, to properly take advantage of these features, we need a global keybinding.

My window manager is i3, which allows you to configure keybindings by editing ~/.config/i3/config. We can add a binding like this:

bindsym $mod+space fullscreen disable; exec "emacsclient -ne '(call-interactively (quote md/alfred))'"

When I press $mod+space, it will now disable fullscreen, execute Emacs and call (md/alfred). I use emacsclient because it's significantly faster than starting a new Emacs instance.

7.1. Floating window

To ensure that the window doesn't get tiled by i3, I set frames marked as "alfred" to be floating:

for_window [class="^Emacs$" instance="^alfred$"] floating enable

You can also use this selection to enable other custom parameters for the frame. For example, I can set a border width:

for_window [class="^Emacs$" instance="^alfred$"] border pixel 1

8. The end

That's it. I've been happily using the described setup for six months now (with a couple of extra features).

9. Why?

Aside from the mega fun we just had, I think there are some genuine upsides:

  • It can support cross platform. Having the same launcher and fuzzy features between OSes seems really useful. It will require some platform-specific code (eg. to launch programs appropriately), but that's not a huge amount of work.
  • Compared to Spotlight, Alfred, and even Albert, it's really easy to edit. I can even do it on the fly - just eval something in Emacs and I'll see the result immediately. You have close control over appearance if that's important to you: you can set Helm faces, frame parameters, etc. You also have fully customisable keybindings - it's Emacs, so you can do whatever you want there.
  • When I used Alfred, I found myself installing it on different machines, and having to manually set up my preferred searches etc. each time. Now it's just part of my dotfiles and works automatically.
  • It means one less program to care about.

10. Next steps

There are a few features that I'd like to expand:

  • A proper .desktop-aware program launcher.
  • A calculator that can eval basic math on the fly (without me having to write it as lisp).
  • A dictionary lookup feature. A workaround is to add a search feature for a dictionary website, but something more native would be nice.
  • A clipboard manager: this has the most unknowns for me. Alfred had features where it could retrieve clipboard history, but I'm not sure if this is achievable via Emacs. If anybody has any pointers then I'd be very happy to hear them.

You can look through my init.el to see more details of my implementation.