It is not astonishing at all that many people (including me) use Dired as their main file manager. The default look of Dired – just the output of ls -l
– is deceptively crude, but underneath there is an immense power. Many Dired commands are just frontends to GNU Coreutils, but with much improved user interface, like reasonable default arguments (accessible via the “future history” of the minibuffer). But Dired is more than just a wrapper around Coreutils. For example, it has the dired-show-file-type
command (bound to y
by default), which runs the file
command on the file at point. The file command is a great way to learn the basic information about any file. Besides telling the filetype, it often provides some information about its contents. For example, it guesses the encoding of text files and shows the resolution of image files.
There are some information it does not give, however. (Note: this is not a criticism of file
, just a plain statement of the fact!) I often deal with media files, and one of the most useful piece of information about a media file is its duration. Another one is the resolution (of course, this makes more sense for video files than for sounds – but even mp3s often have a cover image embedded, so it could make sense to show that for them, too).
This means I have two problems to solve. The first one is to be able to actually gather the duration and resolution data about the media file. I used an LLM to tell me how to invoke ffprobe
to do that and then studied the manpage to understand its output. I have to admit that it worked pretty well, although I modified it a little bit. Here is what I’m going to use to obtain the data:
ffprobe -loglevel error \ -select_streams v:0 \ -show_entries stream=width,height:format=duration \ -output_format csv=p=0 \ -sexagesimal \ filename
Of course, wrapping this in Elisp is fairly easy. Since I need to transform the output of ffprobe
, and process-file
puts it in a buffer, I’m going to use the Emacs approach and do the transformations in the buffer instead of using strings.
(defun get-media-file-metadata (file) "Get FILE's duration and resolution, assuming it’s a media file." (let (process-file-side-effects) (with-temp-buffer (process-file "ffprobe" nil t nil "-loglevel" "error" "-select_streams" "v:0" "-show_entries" "format=duration:stream=width,height" "-output_format" "csv=p=0" "-sexagesimal" file) (goto-char (point-min)) (when (looking-at-p "[0-9]+,[0-9]+") (insert "resolution: ") (search-forward "," nil t) (delete-char -1) (insert "x") (search-forward "\n") (delete-char -1) (insert ", ")) (insert "duration: ") (buffer-substring-no-properties (point-min) (1- (point-max))))))
The other problem is to know when to run ffprobe
– it doesn’t make much sense to run it for text files, for example. Let’s assume that I want to run it for audio and video files only. The go-to tool to check the MIME type of a given file is – you’ve guessed it – file
again. The code is simple, though the if
at the end may be a bit mysterious – its purpose is to return a meaningless result (an empty string) instead of erroring out when file
is a broken symlink.
(defun get-file-mime-type (file deref-symlinks) "Get the mime type of FILE, according to the `file' command." (let (process-file-side-effects) (with-temp-buffer (process-file "file" nil t nil "--brief" "--mime-type" (if deref-symlinks "--dereference" "--no-dereference") file) (buffer-substring-no-properties (point-min) (progn (goto-char (point-min)) (if (search-forward "/" nil t) (1- (point)) (point-min)))))))
Now, the hard part is this. Assuming I want to advise dired-show-file-type
, how do I get its result? (By the way, I could remap it instead of using advice. The difference is subtle – advising also works when I invoke it via M-x dired-show-file-type
, so I figured this is the better solution here. There are some caveats, though.) The situation seems hopeless, since it message
s the result, not returns it. However, there is a light in the tunnel. First of all, message
is the last function it calls, and (as I learned while researching this), message
indeed returns the string it displays. If only there was a way to make message
not to display anything, just return it, I would be good.
Well, it turns out that there is – apparently, there are even two ways of doing that!
One of them is something I’ve already used a few times. Recently, I’ve learned about another one. Emacs has a variable called set-message-function. You can set it to a function which can transform the message in any way you like – for example, concatenate it with something – or even cause it not to be displayed. However, I discovered that it doesn’t influence the string added to the *Messages*
buffer, so it is not that useful for this particular case.
Anyway, the only thing left now is to gather all I know into a piece of Elisp code. One (slightly) tricky challenge is a syntactical one – I need to run the original function with message displaying (and logging) disabled (which means inside let
setting inhibit-message
and message-log-max
), but on the other hand I need to store its result (the original message) and use it outside that let
(to actually display and log the augmented message). When you’re accustomed to imperative programming, the first solution that comes to mind may be this:
(let (msg) (let ((inhibit-message t) (message-log-max nil)) (setq msg (funcall orig-function ...))) (message (concat msg ...)))
but of course the setq
here is really ugly. Of course, we can do better.
(defun get-file-metadata-if-media-file (file deref-symlinks) "If FILE is a audio or video file, get its duration and resolution." (when (member (get-file-mime-type file deref-symlinks) '("audio" "video")) (get-media-file-metadata file))) (defun dired-show-file-type-media-advice (orig-function file deref-symlinks) "Augment the information about file type for media files. This is a function to be attached as `:around' advice to `dired-show-file-type'." (let ((msg (let ((inhibit-message t) (message-log-max nil)) (funcall orig-function file deref-symlinks))) (media-msg (get-file-metadata-if-media-file file deref-symlinks))) (message (if media-msg (concat msg ", " media-msg) msg)))) (advice-add 'dired-show-file-type :around #'dired-show-file-type-media-advice)
Again, if this code were to be distributed as a package, I would probably allow to turn this on and off via a global minor mode, but as this is something I’d like to have always on, I just put all the code (together with the advice) in my init.el
.
I think if there are any other types of files where this approach could be useful, all this could be made more general. For example, there could be an alist where the keys are the MIME types and the values are functions returning strings with additional description to be displayed by dired-show-file-type
. For example, I could write some code to show pagecounts for pdfs or document classes for LaTeX files. Maybe one day I will extend and publish this code as a proper package!