Teaching Emacs to Play Nicely With MPV (No, Really!)

Table of Contents
/img/emacs-mpv/emacs-mpv.png
Opening media file with Spc-f-l

Introduction: The Text Editor That Does Everything (Except Play MP4s… With SPC f l… Natively)

So, you've heard whispers of Emacs. Maybe you heard it's just a text editor. Maybe you heard it's an operating system disguised as a text editor. Both are kind of true. Emacs is incredibly powerful and can be customised to do almost anything.

But getting started can be… daunting. That's where distributions like Doom Emacs come in. Doom bundles up Emacs with a ton of performance tweaks, useful packages, and Vim-like keybindings (hello, SPC key!) to give you a fantastic experience right away. When I began using Doom Emacs, I was reminded heavily of SpaceVim which I used for a while. Perhaps Doom Emacs is what inspired SpaceVim?

One of the superpowers Doom Emacs gives you is the ability to find files anywhere on your system, instantly. You hit a magical key combination (SPC f l in Doom), type part of a filename, and bam, there it is! Ooft. It gives me fuzzy (find) feelings :).

Here was my dream: Hit SPC f l, type 'give up', see my legally acquired audio file, hit Enter (RET in Emacs lingo), and have it instantly start playing in MPV, everyone's favourite media player.

The reality? Hitting RET opened the WEBM file as text inside Emacs. Cue screen full of gibberish. Not exactly the concert I had in mind.

This is the tale of how, I taught Doom Emacs a new trick. And if you're new to Emacs Lisp (elisp - the language Emacs speaks) or Doom, don't worry! I'll break it down.

Step 1: Finding Your Files Blazingly Fast (SPC f l)

First off, the finding part works like a charm out of the box. In Doom Emacs:

  1. Press SPC (the Space bar, Doom's leader key).
  2. Press f (for File).
  3. Press l (for Locate).

This command (consult-locate under the hood) uses your system's file index (created by tools like locate, plocate, or mlocate - think Spotlight or Windows Search, but just for filenames and often faster) to show you matching files instantly. Note: files are required to be indexed first by the filesystems updatedb. This usually occurs automatically at least once-per-day. Should you be impatient to get your lovely new files into the database though.. I got you: sudo updatedb.

+----------------------------------------------------------------+
| Files matching 'give up':                                      |
| /home/user/music/Rick Astley - never gonna give you up.webm    |
| /home/user/music/misc/Jason Mraz - i wont give up.flac         |
| ...                                                            |
+----------------------------------------------------------------+
| > give up |
+----------------------------------------------------------------+
Using 'SPC f l' to find a file

Okay, finding is easy. Step one, complete. But what about step two - opening it correctly?

Step 2: Doing Things with Found Files (The Manual Way)

Doom Emacs, usually via the Vertico package for displaying candidates and the Embark package for actions, lets you do more than just open files. When you have a file selected in that list:

  1. Press C-; (Control + Semicolon). This is typically bound to M-x embark-act. For me, while it said this is the binding, using it resulted in an undefined message. No problem use M-x embark-act to trigger the next step.
  2. A menu pops up with actions you can take on that file using embark-act: copy path, rename, delete, open in Emacs, Open Externally… Aha!

Choosing "Open Externally" usually works! It uses system commands (xdg-open on Linux, open on macOS and absolutely no idea on Windows) to open the file in its default application. If MPV is your default (and it should be) for `.mp3` files, it opens in MPV.

But… SPC f l, type, select, M-x embark-act, choose action (x)… too many steps! We want the pure, unadulterated joy of just hitting RET! Afterall, we're using linux! Everything should be at a few flicks of our fingers.

Step 3: The Quest for `RET` - Why Is It So Hard?

You'd think making RET do something different would be easy, right? Well, in Emacs, RET is prime real estate. Different parts of Emacs want to control what happens when you press it, especially in the "minibuffer" (that little area at the bottom (or in my case, centred) where you type commands or search results).

When you use SPC f l, the list of files is shown by a package called Vertico (in most modern Doom setups). Vertico has its own ideas about what RET should do – usually, "finish completion and return the selected item".

So, our challenge is to intercept that RET press after finding a file with SPC f l but before Emacs tries to open it as text.

Step 4: First Attempts (And Why They Failed) - A Mini Elisp Adventure!

Time to dive into Emacs Lisp, or "elisp". This is the language Emacs is written in, and the language you use to configure it. In Doom Emacs, your personal configuration usually lives in files within `~/.doom.d/`, primarily `config.el`.

Elisp Basics:

  • Code is made of lists in parentheses: `(function argument1 argument2)` (Don't throw up just yet!).
  • `defun` defines a new function (a command or helper).
  • `defvar` defines a variable (a place to store data).
  • `if` checks a condition.
  • We use specific functions to interact with Emacs (like getting filenames) or the system (like running `mpv`).

Attempt A: Teaching Embark Directly

My first thought was to tell Embark (the action menu guy) what RET should do for files. I wrote some elisp code (using functions like `tfg/is-media-file-p` to check extensions and `tfg/embark-open-media-or-find-file` to either run `mpv` or open normally). The core idea was to use `embark-target` to figure out the selected file.

;; Conceptual code - THIS FAILED!
(defun tfg/embark-open-media-or-find-file (arg)
  ;; ... other code ...
  (let* ((target (embark-target)) ; <== Try to get the selected file from Embark
         (target-file (embark-target-filename target))
         ;; ... more code ...
         ))
  ;; ... other code ...
)

;; Try to bind RET in Embark's file map
;; (map! :map embark-file-map "<return>" #'tfg/embark-open-media-or-find-file)

Failure! We got errors like `Symbol’s function definition is void: embark-target`. In plain English: "I have no idea what the heck this`embark-target` instruction is!". Even wrapping it in code to ensure Embark was loaded (`with-eval-after-load 'embark`) didn't fix it reliably in this context. The timing was wrong, or the context wasn't right for `embark-target` to work when triggered by `RET`.

Attempt B: Teaching Emacs's "Do What I Mean" (`embark-dwim`)

Embark has a feature called `embark-dwim` (Do What I Mean) which tries to guess the best default action. I tried configuring this using `embark-dwim-rules`, telling it "If the file is media, the DWIM action is our custom `mpv`-calling function".

;; Conceptual code - THIS ALSO FAILED FOR RET!
;; (We defined tfg/is-media-file-p to check extensions)
(add-to-list 'embark-dwim-rules
             '(file tfg/is-media-file-p tfg/embark-open-media-or-find-file))

Failure again! While this approach seemed more correct, hitting `RET` still opened the file as text. Why?

The Revelation (Debugging `RET`)

Using Emacs's built-in help required some alternate keys (like trying F1 k <key> instead of C-h k <key>, because C-h was disabled in the SPC f l context). With this, I finally checked what command RET was actually running in that file list. It wasn't `embark-dwim`, it wasn't our custom function… it was `vertico-exit`!

Vertico, the package showing the list, was grabbing RET first. Its job was just to exit the list and hand the selected filename back to the original command (consult-locate). And `consult-locate`'s default behaviour? Open the file as text. We were trying to modify the wrong part of the chain!

Step 5: The Breakthrough - Giving `vertico-exit` Some Advice!

If we can't easily change what RET is bound to, maybe we can change what `vertico-exit` does? Enter Emacs "advice".

Advice lets you wrap extra code around an existing function without completely rewriting it. It's like giving someone last-minute instructions just before they perform a task.

Our plan:

  1. Add `:before` advice to `vertico-exit`.
  2. In the advice, check the file Vertico is about to exit with.
  3. If it's a media file: Run `mpv` and then exit the minibuffer ourselves, preventing the original `vertico-exit` from doing anything else.
  4. If it's not a media file: Do nothing, letting the original `vertico-exit` run as usual.

Step 6: The Code That Finally Worked

This led me to the final code. If you wanna use it, add this block to your `~/.doom.d/config.el`:

;; ==========================================================================
;; Spc-f-l is amazing, but let's not default to opening located files as text
;; buffers. Here's some rules to open our media files in MPV :)
;; ==========================================================================
(with-eval-after-load 'vertico
  ;; Ensure this code runs only after Vertico is loaded.

  ;; Extensions we want to open with MPV.
  (defvar tfg-media-extensions
    '("mp4" "mkv" "avi" "mov" "wmv" "flv" "webm" ;; Video.
      "mp3" "flac" "ogg" "opus" "wav" "m4a" "aac") ;; Audio.
    ;; Might put Image into this list later...
    "List of file extensions to open with mpv.")

  ;; Check if a target is a media file.
  (defun tfg/is-media-file-p (target-file)
    "Return non-nil if TARGET-FILE string has a media extension."
    ;; Check if we actually got a filename string.
    (when (stringp target-file)
      ;; Get the part after the last dot (.).
      (let ((extension (file-name-extension target-file)))
        ;; Check if we got an extension and if it's in our list (case-insensitive).
        (and extension (member (downcase extension) tfg-media-extensions)))))

  ;; Define the :before advice for vertico-exit.
  (defun tfg/vertico-exit-advice (&rest _args)
    "Advice for `vertico-exit` run :before.
If the current candidate is a media file, open it with mpv
and exit the minibuffer, preventing original `vertico-exit`."
    ;; Get the candidate that vertico is about to exit with.
    ;; NOTE: Using internal `vertico--candidate`, might break if vertico changes.
    ;; This is a peek into Vertico's internal state!
    (let ((candidate (vertico--candidate)))
      ;; Check if it's one of our media files using our helper function.
      (when (tfg/is-media-file-p candidate)
        ;; It IS! It IS a media file: (get it? like.. I did see a puddy cat!).
        (message "Opening %s in mpv..." (file-name-nondirectory candidate))
        ;; Run mpv asynchronously (so Emacs doesn't freeze).
        ;; shell-quote-argument handles spaces/weird chars in filenames safely!
        (call-process-shell-command (format "mpv %s" (shell-quote-argument candidate)) nil nil nil)
        ;; Exit the minibuffer NOW! - This stops the original vertico-exit.
        (minibuffer-exit))))

  ;; Add the advice to vertico-exit: Run our function BEFORE vertico-exit.
  (advice-add 'vertico-exit :before #'tfg/vertico-exit-advice)
)

Breaking Down the Magic:

  • `(with-eval-after-load 'vertico …)`: Waits for `vertico` to load before trying to add advice to it. Crucial for timing!
  • `(defvar tfg-media-extensions …)`: Defines your list of target extensions. Customize it! (The `tfg/` prefix is just my personal convention for my functions/variables).
  • `(defun tfg/is-media-file-p …)`: Our helper check. Takes a filename string, extracts the extension, converts to lowercase, and sees if it's in our list.
  • `(defun tfg/vertico-exit-advice …)`: This is the core advice function.

    • `&rest _args`: Accepts any arguments `vertico-exit` might receive but ignores them (using `_args` conventionally signals they aren't used).
    • `(vertico–candidate)`: This is how we ask Vertico "what item is currently selected?". Heads up: Functions starting with `–` are often internal - they might change in future Vertico updates, potentially breaking this code. It's a small risk for a neat feature.
    • `(when (tfg/is-media-file-p candidate) …)`: If the selected item is a media file…
    • `(message …)`: Show a helpful message in Emacs.
    • `(call-process-shell-command …)`: The command to run `mpv`. `format` builds the string "mpv 'filename'", `shell-quote-argument` makes it safe for the shell, `nil nil nil` runs it in the background without messing with Emacs buffers.
    • `(minibuffer-exit)`: The key! This command closes the minibuffer successfully. Because our advice runs `:before` `vertico-exit`, and this exits the minibuffer, the original `vertico-exit` function never gets executed for media files.
  • `(advice-add …)`: This connects our advice function to the original `vertico-exit`.

Installation and Final Thoughts

  1. Make sure you have `mpv` installed and your system knows where to find it (it's in your `PATH`).
  2. Paste the final code block into your Doom Emacs configuration file at `~/.doom.d/config.el`.
  3. Customize the `tfg-media-extensions` list.
  4. Restart Emacs. (Or maybe run `doom sync` in the terminal and then restart).

And that's it! Now, when we use SPC f l, find that song or video, and hit RET, MPV springs to life!

This little hacking adventure shows the power (and sometimes the complexity) of Emacs customisation. We hit dead ends, debugged weird errors, and finally landed on a solution using 'advice'. It's a glimpse into how you can bend Emacs to almost any workflow you can imagine.

Now! at 3:30am, it's time to push this blog post and head to sleep!

– Happy Hacking!