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:
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:
- Create a buffer named
*alfred*
. - Make a new "frame" (ie. a new X window) for this buffer, applying some parameters to resize and bring it into focus.
- Set variables to disable the mode-line and the message area, and apply some Helm styling parameters.
- Call Helm with a list of custom "sources" (which we'll get to next), telling it to use our new buffer.
- 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
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
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
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.
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.