Emacs: flat Dired listing for REGEXP, optionally up to DAYS since last modification

When we call dired from Lisp, we can pass it a list of files instead of a directory. This gives us a fully fledged Dired buffer for those files. My most common use-case is to produce flat listing, so that I do not have to go searching in exactly which directory some file is (e.g. in the Downloads folder there is some zip archive that I downloaded with a bunch of files in a complex structure).

A flat Dired listing

For a while now I have been using my own command to create a Dired buffer from the current directory (which can always be updated on demand with M-x cd). It is prot-dired-search-flat-list. Here is the code:

(defvar prot-dired-regexp-history nil
  "Minibuffer history of `prot-dired-regexp-prompt'.")

(defun prot-dired-regexp-prompt ()
  (let ((default (car prot-dired-regexp-history)))
    (read-regexp
     (format-prompt "Files matching REGEXP" default)
     default 'prot-dired-regexp-history)))

(defun prot-dired--get-files (regexp)
  "Return files matching REGEXP, recursively from `default-directory'."
  (directory-files-recursively default-directory regexp nil))

;;;###autoload
(defun prot-dired-search-flat-list (regexp)
  "Return a Dired buffer for files matching REGEXP.
Perform the search recursively from the current directory."
  (interactive (list (prot-dired-regexp-prompt)))
  (if-let* ((files (prot-dired--get-files regexp))
            (relative-paths (mapcar #'file-relative-name files)))
      (dired (cons (format "prot-flat-dired for `%s'" regexp) relative-paths))
    (error "No files matching `%s'" regexp)))

I could modify prot-dired-search-flat-list to also prompt for a directory, though I optimise for the common workflow of operating from where I am (and I generally do not like overloading the C-u with special cases that I will never remember—a new command with a name I can search for is better).

Flat listing limited to last modified since DAYS

Yesterday I had the need to browse a massive directory, but only wanted to get a couple of files out of it. I realised that I had to filter my last modified, so I extended my above use-case with the new command prot-dired-search-flat-list-since-days. Here is what I came up with:

(defvar prot-dired-days-prompt-history nil
  "Minibuffer history for `prot-dired-days-prompt'.")

(defun prot-dired-days-prompt ()
  "Prompt for days and return them as a number."
  (let* ((first (car prot-dired-days-prompt-history))
         (default (when (stringp first)
                    (string-to-number first))))
    (read-number "Number of days: " default 'prot-dired-days-prompt-history)))

(defun prot-dired--get-last-modified (files days)
  "Return list of FILES last modified since DAYS."
  (seq-filter
   (lambda (file)
     (and-let* ((attributes (file-attributes file))
                (last-modified (nth 5 attributes))
                (last-modified-seconds (time-to-seconds last-modified))
                (current-time (current-time))
                (current-time-seconds (time-to-seconds current-time))
                (delta-seconds (* days 24 60 60))
                (oldest-seconds (- current-time-seconds delta-seconds))
                (_ (>= last-modified-seconds oldest-seconds)))))
   files))

;;;###autoload
(defun prot-dired-search-flat-list-since-days (regexp days)
  "Return Dired buffer with files matching REGEXP up to DAYS since last modification.
Perform the search recursively from the current directory."
  (interactive
   (list
    (prot-dired-regexp-prompt)
    (prot-dired-days-prompt)))
  (if-let* ((files (prot-dired--get-files regexp)))
      (if-let* ((files-filtered (prot-dired--get-last-modified files days))
                (relative-paths (mapcar #'file-relative-name files-filtered)))
          (dired (cons (format "prot-flat-dired since %d days for `%s'" days regexp) relative-paths))
        (error "No files last modified within the last %d days" days))
    (error "No files matching `%s'" regexp)))

Note that I always design my minibuffer prompts to have their own history, because then I only get relevant entries when I press M-p (previous-history-element) and M-n (next-history-element) at the prompt (and the built-in savehist-mode takes care to persist those).

Everything is part of my Emacs configuration: https://protesilaos.com/emacs/dotemacs. I will not be updating this article, so make sure to check for any further refinements there.