Nicolas Martyanoff – Brain dump About

Decluttering Dired for peace of mind

Dired is an Emacs mode providing an interactive interface to navigate and manipulate files. It took me a while to start using it, but it is so efficient that I now spend more time in it than in my usual shell.

However I am no fond of the way files are listed. Dired displays a lot of information which is — at least for me — rarely used. As a result the important content (the name of the file) is flushed to the right of the window, often leading to unpractical line wrapping.

It is hard to find what you are searching for in this kind of interface:

Dired default view

So I decided to spend some time improving my Dired experience.

Configuring a simpler view

Dired comes with the dired-hide-details-mode minor mode which hides extra information. By default it is activated with the ( key.

It has two drawbacks:

  • First it also hides the display of symbolic link target while still showing the -> marker after the link. This behaviour can be changed by setting the dired-hide-details-hide-symlink-targets variable to nil.
  • Second, Dired disables this minor mode each time you move to a different directory, which is really annoying

We create a g-dired-minimal-view variable to store the current state. When then write two functions: one to either enable or disable the minor mode depending on the state, called by the dired-mode-hook, and one to switch between simple and extended view bound to the more accessible <tab> key.

(setq g-dired-minimal-view t)

(defun g-dired-setup-view ()
  (dired-hide-details-mode (if g-dired-minimal-view 1 -1)))

(defun g-dired-switch-view ()
  (interactive)
  (setq g-dired-minimal-view (not g-dired-minimal-view))
  (g-dired-setup-view))

(use-package dired
  :config
  (setq dired-hide-details-hide-symlink-targets nil)

  :hook
  ((dired-mode-hook . g-dired-setup-view))

  :bind
  (:map dired-mode-map
        ("<tab>" . g-dired-switch-view)))

A better extended view

While I prefer the simple view by default, the extended view still has its use; but I find it hard to read:

  • The traditional representations of permissions takes a lot of horizontal space, the 4 digit octal value would be shorter.
  • The number of links is not really useful.
  • The size of the file would be more convenient in a human readable format.
  • The date and time uses an irregular US-style format which is either “month day year” or “month day hour:minute”.

To list files, Dired call the ls program with a list of options (defined by the dired-listing-switches variable), insert the output in the buffer and rely on regular expressions to extract information. This means that we have very little control on formatting. We can change the options passed to ls, but we have to be careful not to break Dired features. For example, Dired can highlight files with specific permissions, but rely on a regular expression based on the format used by ls, so we cannot change the permission column.

First we use dired-listing-switches to add the -h (for human-readable sizes) and --time-style=long-iso option. Since these options are only available for the GNU version of ls, I will have to add platform detection for FreeBSD support in a way that works when Dired is running with TRAMP. Something for another day.

Then we add a dired-after-readin-hook hook which is called by Dired after the output of ls has been collected and inserted in the buffer. In this hook, we iterate on lines and process them. We highlight metadata with the shadow face to keep them visually distinct from the filename, and we hide the link count column using the invisible text property so that the text stays in the buffer, making sure not to break Dired features.

Finally we disable wrapping with a hook.

(defun g-dired-postprocess-ls-output ()
  "Postprocess the list of files printed by the ls program when
executed by Dired."
  (save-excursion
    (goto-char (point-min))
    (while (not (eobp))
      ;; Go to the beginning of the next line representing a file
      (while (null (dired-get-filename nil t))
        (dired-next-line 1))
      (beginning-of-line)
      ;; Narrow to the line and process it
      (let ((start (line-beginning-position))
            (end (line-end-position)))
        (save-restriction
          (narrow-to-region start end)
          (setq inhibit-read-only t)
          (unwind-protect
              (g-dired-postprocess-ls-line)
            (setq inhibit-read-only nil))))
      ;; Next line
      (dired-next-line 1))))

(defun g-dired-disable-line-wrapping ()
  (setq truncate-lines t))

(defun g-dired-postprocess-ls-line ()
  "Postprocess a single line in the ls output, i.e. the information
about a single file. This function is called with the buffer
narrowed to the line."
  ;; Highlight everything but the filename
  (when (re-search-forward directory-listing-before-filename-regexp nil t 1)
    (add-text-properties (point-min) (match-end 0) '(font-lock-face shadow)))
  ;; Hide the link count
  (beginning-of-line)
  (when (re-search-forward " +[0-9]+" nil t 1)
    (add-text-properties (match-beginning 0) (match-end 0) '(invisible t))))

(use-package dired
  :config
  (setq dired-listing-switches "-alh --time-style=long-iso")

  :hook
  ((dired-mode-hook . g-dired-disable-line-wrapping)
   (dired-after-readin-hook . g-dired-postprocess-ls-output)))

Much better now!

Dired extended view

Share the word!

Liked my article? Follow me on Twitter or on Mastodon to see what I'm up to.