300 lines
12 KiB
EmacsLisp
300 lines
12 KiB
EmacsLisp
;;; dired-ranger.el --- Implementation of useful ranger features for dired
|
||
|
||
;; Copyright (C) 2014-2015 Matúš Goljer
|
||
|
||
;; Author: Matúš Goljer <matus.goljer@gmail.com>
|
||
;; Maintainer: Matúš Goljer <matus.goljer@gmail.com>
|
||
;; Version: 0.0.1
|
||
;; Package-Version: 20180401.2206
|
||
;; Package-Commit: 7c0ef09d57a80068a11edc74c3568e5ead5cc15a
|
||
;; Created: 17th June 2014
|
||
;; Package-requires: ((dash "2.7.0") (dired-hacks-utils "0.0.1"))
|
||
;; Keywords: files
|
||
|
||
;; This program is free software; you can redistribute it and/or
|
||
;; modify it under the terms of the GNU General Public License
|
||
;; as published by the Free Software Foundation; either version 3
|
||
;; of the License, or (at your option) any later version.
|
||
|
||
;; This program is distributed in the hope that it will be useful,
|
||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
;; GNU General Public License for more details.
|
||
|
||
;; You should have received a copy of the GNU General Public License
|
||
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
;;; Commentary:
|
||
|
||
;; This package implements useful features present in the
|
||
;; [ranger](http://ranger.github.io/) file manager which are missing
|
||
;; in dired.
|
||
|
||
;; Multi-stage copy/pasting of files
|
||
;; ---------------------------------
|
||
|
||
;; A feature present in most orthodox file managers is a "two-stage"
|
||
;; copy/paste process. Roughly, the user first selects some files,
|
||
;; "copies" them into a clipboard and then pastes them to the target
|
||
;; location. This workflow is missing in dired.
|
||
|
||
;; In dired, user first marks the files, then issues the
|
||
;; `dired-do-copy' command which prompts for the destination. The
|
||
;; files are then copied there. The `dired-dwim-target' option makes
|
||
;; this a bit friendlier---if two dired windows are opened, the other
|
||
;; one is automatically the default target.
|
||
|
||
;; With the multi-stage operations, you can gather files from
|
||
;; *multiple* dired buffers into a single "clipboard", then copy or
|
||
;; move all of them to the target location. Another huge advantage is
|
||
;; that if the target dired buffer is already opened, switching to it
|
||
;; via ido or ibuffer is often faster than selecting the path.
|
||
|
||
;; Call `dired-ranger-copy' to add marked files (or the file under
|
||
;; point if no files are marked) to the "clipboard". With non-nil
|
||
;; prefix argument, add the marked files to the current clipboard.
|
||
|
||
;; Past clipboards are stored in `dired-ranger-copy-ring' so you can
|
||
;; repeat the past pastes.
|
||
|
||
;; Call `dired-ranger-paste' or `dired-ranger-move' to copy or move
|
||
;; the files in the current clipboard to the current dired buffer.
|
||
;; With raw prefix argument (usually C-u), the clipboard is not
|
||
;; cleared, so you can repeat the copy operation in another dired
|
||
;; buffer.
|
||
|
||
;; Bookmarks
|
||
;; ---------
|
||
|
||
;; Use `dired-ranger-bookmark' to bookmark current dired buffer. You
|
||
;; can later quickly revisit it by calling
|
||
;; `dired-ranger-bookmark-visit'.
|
||
|
||
;; A bookmark name is any single character, letter, digit or a symbol.
|
||
|
||
;; A special bookmark with name `dired-ranger-bookmark-LRU' represents
|
||
;; the least recently used dired buffer. Its default value is `. If
|
||
;; you bind `dired-ranger-bookmark-visit' to the same keybinding,
|
||
;; hitting `` will instantly bring you to the previously used dired
|
||
;; buffer. This can be used to toggle between two dired buffers in a
|
||
;; very fast way.
|
||
|
||
;; These bookmarks are not persistent. If you want persistent
|
||
;; bookmarks use the bookmarks provided by emacs, see (info "(emacs)
|
||
;; Bookmarks").
|
||
|
||
;;; Code:
|
||
|
||
(require 'dired-hacks-utils)
|
||
(require 'dash)
|
||
(require 'ring)
|
||
|
||
(defgroup dired-ranger ()
|
||
"Implementation of useful ranger features for dired."
|
||
:group 'dired-hacks
|
||
:prefix "dired-ranger-")
|
||
|
||
|
||
;; multi-stage copy/paste operations
|
||
(defcustom dired-ranger-copy-ring-size 10
|
||
"Specifies how many filesets for copy/paste operations should be stored."
|
||
:type 'integer
|
||
:group 'dired-ranger)
|
||
|
||
(defvar dired-ranger-copy-ring (make-ring dired-ranger-copy-ring-size))
|
||
|
||
;;;###autoload
|
||
(defun dired-ranger-copy (arg)
|
||
"Place the marked items in the copy ring.
|
||
|
||
With non-nil prefix argument, add the marked items to the current
|
||
selection. This allows you to gather files from multiple dired
|
||
buffers for a single paste."
|
||
(interactive "P")
|
||
;; TODO: add dired+ `dired-get-marked-files' support?
|
||
(let ((marked (dired-get-marked-files)))
|
||
(if (or (not arg)
|
||
(ring-empty-p dired-ranger-copy-ring))
|
||
(progn
|
||
(ring-insert
|
||
dired-ranger-copy-ring
|
||
(cons (list (current-buffer)) marked))
|
||
;; TODO: abstract the message/plural detection somewhere
|
||
;; (e.g. give it a verb and number to produce the correct
|
||
;; string.)
|
||
(message (format "Copied %d item%s into copy ring."
|
||
(length marked)
|
||
(if (> (length marked) 1) "s" ""))))
|
||
(let ((current (ring-remove dired-ranger-copy-ring 0)))
|
||
(ring-insert
|
||
dired-ranger-copy-ring
|
||
(cons (-distinct (cons (current-buffer) (car current)))
|
||
(-distinct (-concat (dired-get-marked-files) (cdr current)))))
|
||
(message (format "Added %d item%s into copy ring."
|
||
(length marked)
|
||
(if (> (length marked) 1) "s" "")))))))
|
||
|
||
(defun dired-ranger--revert-target (char target-directory files)
|
||
"Revert the target buffer and mark the new files.
|
||
|
||
CHAR is the temporary value for `dired-marker-char'.
|
||
|
||
TARGET-DIRECTORY is the current dired directory.
|
||
|
||
FILES is the list of files (from the `dired-ranger-copy-ring') we
|
||
operated on."
|
||
(let ((current-file (dired-utils-get-filename)))
|
||
(revert-buffer)
|
||
(let ((dired-marker-char char))
|
||
(--each (-map 'file-name-nondirectory files)
|
||
(dired-utils-goto-line (concat target-directory it))
|
||
(dired-mark 1)))
|
||
(dired-utils-goto-line current-file)))
|
||
|
||
;;;###autoload
|
||
(defun dired-ranger-paste (arg)
|
||
"Copy the items from copy ring to current directory.
|
||
|
||
With raw prefix argument \\[universal-argument], do not remove
|
||
the selection from the stack so it can be copied again.
|
||
|
||
With numeric prefix argument, copy the n-th selection from the
|
||
copy ring."
|
||
(interactive "P")
|
||
(let* ((index (if (numberp arg) arg 0))
|
||
(data (ring-ref dired-ranger-copy-ring index))
|
||
(files (cdr data))
|
||
(target-directory (dired-current-directory))
|
||
(copied-files 0))
|
||
(--each files (when (file-exists-p it)
|
||
(if (file-directory-p it)
|
||
(copy-directory it target-directory)
|
||
(condition-case err
|
||
(copy-file it target-directory 0)
|
||
(file-already-exists nil)))
|
||
(cl-incf copied-files)))
|
||
(dired-ranger--revert-target ?P target-directory files)
|
||
(unless arg (ring-remove dired-ranger-copy-ring 0))
|
||
(message (format "Pasted %d/%d item%s from copy ring."
|
||
copied-files
|
||
(length files)
|
||
(if (> (length files) 1) "s" "")))))
|
||
|
||
;;;###autoload
|
||
(defun dired-ranger-move (arg)
|
||
"Move the items from copy ring to current directory.
|
||
|
||
This behaves like `dired-ranger-paste' but moves the files
|
||
instead of copying them."
|
||
(interactive "P")
|
||
(let* ((index (if (numberp arg) arg 0))
|
||
(data (ring-ref dired-ranger-copy-ring index))
|
||
(buffers (car data))
|
||
(files (cdr data))
|
||
(target-directory (dired-current-directory))
|
||
(copied-files 0))
|
||
(--each files (when (file-exists-p it)
|
||
(condition-case err
|
||
(rename-file it target-directory 0)
|
||
(file-already-exists nil))
|
||
(cl-incf copied-files)))
|
||
(dired-ranger--revert-target ?M target-directory files)
|
||
(--each buffers
|
||
(when (buffer-live-p it)
|
||
(with-current-buffer it (revert-buffer))))
|
||
(unless arg (ring-remove dired-ranger-copy-ring 0))
|
||
(message (format "Moved %d/%d item%s from copy ring."
|
||
copied-files
|
||
(length files)
|
||
(if (> (length files) 1) "s" "")))))
|
||
|
||
|
||
;; bookmarks
|
||
(defcustom dired-ranger-bookmark-reopen 'ask
|
||
"Should we reopen closed dired buffer when visiting a bookmark?
|
||
|
||
This does only correctly reopen regular dired buffers listing one
|
||
directory. Special dired buffers like the output of `find-dired'
|
||
or `ag-dired', virtual dired buffers and subdirectories can not
|
||
be recreated.
|
||
|
||
The value 'never means never reopen the directory.
|
||
|
||
The value 'always means always reopen the directory.
|
||
|
||
The value 'ask will ask if we should reopen or not. Reopening a
|
||
dired buffer for a directory that is already opened in dired will
|
||
bring that up, which might be unexpected as that directory might
|
||
come from a non-standard source (i.e. not be file-system
|
||
backed)."
|
||
:type '(radio
|
||
(const :tag "Never reopen automatically." never)
|
||
(const :tag "Always reopen automatically." always)
|
||
(const :tag "Reopen automatically only in standard dired buffers, ask otherwise." ask))
|
||
:group 'dired-ranger)
|
||
|
||
(defcustom dired-ranger-bookmark-LRU ?`
|
||
"Bookmark representing the least recently used/visited dired buffer.
|
||
|
||
If a dired buffer is currently active, select the one visited
|
||
before. If a non-dired buffer is active, visit the least
|
||
recently visited dired buffer."
|
||
:type 'char
|
||
:group 'dired-ranger)
|
||
|
||
(defvar dired-ranger-bookmarks nil
|
||
"An alist mapping bookmarks to dired buffers and locations.")
|
||
|
||
;;;###autoload
|
||
(defun dired-ranger-bookmark (char)
|
||
"Bookmark current dired buffer.
|
||
|
||
CHAR is a single character (a-zA-Z0-9) representing the bookmark.
|
||
Reusing a bookmark replaces the content. These bookmarks are not
|
||
persistent, they are used for quick jumping back and forth
|
||
between currently used directories."
|
||
(interactive "cBookmark name: ")
|
||
(let ((dir (file-truename default-directory)))
|
||
(-if-let (value (cdr (assoc char dired-ranger-bookmarks)))
|
||
(setf (cdr (assoc char dired-ranger-bookmarks)) (cons dir (current-buffer)))
|
||
(push (-cons* char dir (current-buffer)) dired-ranger-bookmarks))
|
||
(message "Bookmarked directory %s as `%c'" dir char)))
|
||
|
||
;;;###autoload
|
||
(defun dired-ranger-bookmark-visit (char)
|
||
"Visit bookmark CHAR.
|
||
|
||
If the associated dired buffer was killed, we try to reopen it
|
||
according to the setting `dired-ranger-bookmark-reopen'.
|
||
|
||
The special bookmark `dired-ranger-bookmark-LRU' always jumps to
|
||
the least recently visited dired buffer.
|
||
|
||
See also `dired-ranger-bookmark'."
|
||
(interactive "cBookmark name: ")
|
||
(if (eq char dired-ranger-bookmark-LRU)
|
||
(progn
|
||
(let ((buffers (buffer-list)))
|
||
(when (eq (with-current-buffer (car buffers) major-mode) 'dired-mode)
|
||
(pop buffers))
|
||
(switch-to-buffer (--first (eq (with-current-buffer it major-mode) 'dired-mode) buffers))))
|
||
(-if-let* ((value (cdr (assoc char dired-ranger-bookmarks)))
|
||
(dir (car value))
|
||
(buffer (cdr value)))
|
||
(if (buffer-live-p buffer)
|
||
(switch-to-buffer buffer)
|
||
(when
|
||
;; TODO: abstract this never/always/ask pattern. It is
|
||
;; also used in filter.
|
||
(cond
|
||
((eq dired-ranger-bookmark-reopen 'never) nil)
|
||
((eq dired-ranger-bookmark-reopen 'always) t)
|
||
((eq dired-ranger-bookmark-reopen 'ask)
|
||
(y-or-n-p (format "The dired buffer referenced by this bookmark does not exist. Should we try to reopen `%s'?" dir))))
|
||
(find-file dir)
|
||
(setf (cdr (assoc char dired-ranger-bookmarks)) (cons dir (current-buffer)))))
|
||
(message "Bookmark `%c' does not exist." char))))
|
||
|
||
(provide 'dired-ranger)
|
||
;;; dired-ranger.el ends here
|