← all posts

Zoxide inside of Emacs.

Minimal Zoxide inside of Emacs

If you're like me, you love using every CLI/TUI tool out there that enhances the terminal experience, some of which are:

  • tmux
  • fzf
  • deja
  • eza
  • bat

and most importantly -> zoxide

The problem is that the more I use Emacs these days, the less I look at the terminal. Most of the things in that list are replaced by the Emacs experience itself… everything except zoxide.


zoxide.el

Now the tldr solution to all of this is to just install zoxide.el from Melpa. However, something about loading a package for this use-case just didn't sit right with me. Zoxide is already installed on the system, I only use one command in the entire workflow. Surely that's just a single defun in my init.el that calls the z command with my user input.

Emacspeak

Now I did have a go at writing the Elisp myself until I found someone already did it over at the emacspeak zoxide blog.

;; zoxide integration: query zoxide and open the result in dired
(defun emacs-zoxide (q)
  "Query zoxide Q and launch Dired."
  (interactive "sZoxide: ")
  (if-let*
      ((zoxide (executable-find "zoxide"))
       (target
        (with-temp-buffer
          (if (= 0 (call-process zoxide nil t nil "query" q))
              (string-trim (buffer-string))))))
      (funcall-interactively #'dired target)
    (unless zoxide (error "Install zoxide"))
    (unless target (error "No Match"))))

I may have made one change to the string output somewhere but all credit goes to this blog. It did most of what I needed, I navigate to where I want using my terminal already, that covers all the essential locations in the zoxide database. Then I simply call emacs-zoxide from within Emacs and boom I'm there!

Everything was perfect… until it wasn't. The more and more I used Emacs, the less I touched the terminal, and the more outdated the Zoxide registry became, it stopped updating and being in sync with my more recent activity and over the weeks and months, Zoxide stopped being useful compared to just standard consult-bookmark, find-file, dired etc…

Now I can either succumb to pressure and install the damn zoxide.el like any normal person would… or I can write just one more defun…

My own additions

Making it automatic

Now lets think about the problem here, all that's really missing is a hook that simply just calls the same process that z ~/path/ in your terminal does, doesn't sound hard enough.

(defun emacs-zoxide-add-dired ()
  "Silently add the current dired dir to zoxide."
  (when-let ((zoxide (executable-find "zoxide")))
    (call-process zoxide nil nil nil "add" (directory-file-name default-directory))))

(add-hook 'dired-mode-hook #'my-zoxide-add-dired-dir-hack)

Following the same convention as the original code, we check if Zoxide is available, if so we call the process to add the current default-directory to Zoxide. Then we just add a hook that calls this function whenever entering a directory in Dired.

Looks good enough, everything synced over to my terminal, but there was one problem. Often times I still used cd over zoxide in the terminal to avoid registering every single folder. Whether you think that's a good idea or not, I only wanted to store the essential locations. The point is I may not want to use zoxide all the time and I'm sure there's gonna be other reasons for this too.

Making it manual

One more defun, but I promise this is the last one. If you only want to manually insert a particulary directory yourself, this is the command that worked for me:

(defun emacs-zoxide-add ()
  "Manually add dir to zoxide."
  (interactive)
  (if-let ((zoxide (executable-find "zoxide")))
      (let* ((raw-dir (read-directory-name "Add to zoxide: " default-directory default-directory t))
             (clean-dir (expand-file-name raw-dir)))
        (if (= 0 (call-process zoxide nil nil nil "add" clean-dir))
            (message "Registered %s in zoxide" (abbreviate-file-name clean-dir))
          (error "Failed to register %s in zoxide" clean-dir)))
    (error "Install zoxide")))

Now this defun was much simpler, before it would just auto-add whatever directory I'm currently sitting on, but then it was obvious that I might want to navigate to a parent directory or some other external dir without manually having to move backwards.

Therefore, this now uses the built-in read-directory-name to grab the dir you want, pre-filled with the current directory.

Note: Always use expand-file-name whenever reading a directory from the user (in clean-dir). I was definitely not banging my head over this.

So now I can ctrl+backspace to a parent, use tab-completion or go anywhere else to a different path externally.

And that's it!

That's all I absolutely needed out of Zoxide inside of Emacs. I hope this may be of use to you and perhaps you can add your own additions too. If you do, please let me know whatever you come up with!


Why not just use bookmarks?

With Zoxide, I don't have to think about doing anything, it handles the insertion and ordering for me. It has a much more intelligent filtering system which automatically learns from your usage, its search handles subdirectories and fuzzy searching very well, you can read more about it here.

Moreover, bookmark searching is simply not as snappy, especially as you add more options in there, which doesn't make it a good candidate to handle your whole filesystem. For this reason, I reserve it for the most frequent 1-9 locations while letting Zoxide handle everything else.

Validate