Quickly move a file to the current directory

Often I’ll download a file in my browser, and then want to move that file to the directory in which I am working in emacs. I wrote a little helper function to streamline this, called bjm/move-file-here, given below or at this github gist. Call the function and it will prompt you with a list of files in your starting directory (defaulting to ~/downloads, but configurable with bjm/move-file-here-start-dir) sorted to have the most recent first. The chosen file will then be moved to the current directory if you are in dired, or else the directory of the current buffer.

The function needs the packages dash.el and swiper installed. Here is the code – comments are welcome.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; move file here                                                         ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(require 'dash)
(require 'swiper)

;; start directory
(defvar bjm/move-file-here-start-dir (expand-file-name "~/downloads"))

(defun bjm/move-file-here ()
  "Move file from somewhere else to here.
The file is taken from a start directory set by `bjm/move-file-here-start-dir' and moved to the current directory if invoked in dired, or else the directory containing current buffer. The user is presented with a list of files in the start directory, from which to select the file to move, sorted by most recent first."
  (interactive)
  (let (file-list target-dir file-list-sorted start-file start-file-full)
    ;; clean directories from list but keep times
    (setq file-list
          (-remove (lambda (x) (nth 1 x))
                   (directory-files-and-attributes bjm/move-file-here-start-dir)))

    ;; get target directory
    ;; http://ergoemacs.org/emacs/emacs_copy_file_path.html
    (setq target-dir
          (if (equal major-mode 'dired-mode)
              (expand-file-name default-directory)
            (if (null (buffer-file-name))
                (user-error "ERROR: current buffer is not associated with a file.")
              (file-name-directory (buffer-file-name)))))

  ;; sort list by most recent
  ;;http://stackoverflow.com/questions/26514437/emacs-sort-list-of-directories-files-by-modification-date
  (setq file-list-sorted
        (mapcar #'car
                (sort file-list
                      #'(lambda (x y) (time-less-p (nth 6 y) (nth 6 x))))))

  ;; use ivy to select start-file
  (setq start-file (ivy-read
                    (concat "Move selected file to " target-dir ":")
                    file-list-sorted
                    :re-builder #'ivy--regex
                    :sort nil
                    :initial-input nil))

  ;; add full path to start file and end-file
  (setq start-file-full
        (expand-file-name start-file bjm/move-file-here-start-dir))
  (setq end-file
        (expand-file-name (file-name-nondirectory start-file) target-dir))
  (rename-file start-file-full end-file)
  (message "moved %s to %s" start-file-full end-file)))
  • Clément Pit–Claudel

    Suggestion: don’t use “concat” for file names: instead, use expand-file-name 🙂
    Neat piece of code, in any case.

    • Thanks. I’ve also got rid of all those nested let bindings. Not sure what I was thinking!

  • Handy. Thanks! Made a small change to make selection generic (ie. for helm, ido, etc):

    (setq start-file (completing-read (format “Move selected file to %s:” target-dir)
    file-list-sorted))

  • Rene Froger

    Would it be possible to also recursive look into directories for files inside the `downloads` directory, which are downloaded to the Downloads folder? For example, the directories that are cloned with Git?

  • Pingback: 2016-04-25 Emacs News - sacha chua :: living an awesome life()

  • Rene Froger

    Any suggestion?

    • Apologies – I don’t know of a way to do this within emacs, but I’m not the best person to ask. I would do it by calling the shell command find, but this seems inelegant and I’m sure there is a better way!

  • Heikki Lehvaslaiho

    I found out that I frequently needed to add the name of the moved file to the document I was editing, so I added commands to copy the short filename to clipboard:
    (kill-new start-file)
    (x-set-selection ‘PRIMARY start-file)

  • disqus_NGUkO53xWm

    Hi!

    Just discovered this neat little snippet, thx so much. i have a small question. suppose i want it to look into 2 directories, can this work? i tried this:

    ;; start directory
    (defvar bjm/move-file-here-start-dir (expand-file-name “~/Downloads” “~/ZH_tmp”))

    yet it only seems to fetch from the first (Downloads)

    Also: can it also go recursive? so if i have a file in ~/Downloads/TEST/file1 it will also shoe file1 in swiper?

    Thx!

    Z

    • I like this idea but it is not trivial to add. The problem is that the function directory-files-and-attributes doesn’t support more than one directory and also does not work recursively. This function is used to give a list of file names and times to allow the list to be sorted for ivy.

      To get the behaviour you want, I think you would need to make three changes:
      1) Replace directory-files-and-attributes with directory-files-recursively to get a recursive list of files in a particular directory
      2) Loop over the list of directories that you want to include (i.e. downloads, ZH_tmp etc) building a list of all the absolute file names
      3) Loop over all of those files using file-attributes to get the modification time and construct a list of filenames sorted on time that can be given to ivy

      I don’t have time to do this right now but agree it is a great idea!

      • disqus_NGUkO53xWm

        thx ben!

        my lisp knowledge (and generally coding skills) are currently zero but i am trying to learn. ill update you if i manage to solve this

        best

        z