├── .gitignore
├── images
├── grid.gif
└── posframe.png
├── README.asc
└── ivy-explorer.el
/.gitignore:
--------------------------------------------------------------------------------
1 | *.elc
2 | *-pkg.el
3 | *-autoloads.el
4 |
--------------------------------------------------------------------------------
/images/grid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clemera/ivy-explorer/HEAD/images/grid.gif
--------------------------------------------------------------------------------
/images/posframe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clemera/ivy-explorer/HEAD/images/posframe.png
--------------------------------------------------------------------------------
/README.asc:
--------------------------------------------------------------------------------
1 | :experimental:
2 |
3 | = Introduction
4 |
5 | ++++
6 |
7 |
8 |
9 | ++++
10 |
11 | Provides a large more easily readable grid for file browsing using
12 | https://github.com/abo-abo/swiper[ivy].
13 |
14 | Users can navigate the grid using kbd:[Ctrl-f], kbd:[Ctrl-b], kbd:[Ctrl-n],
15 | kbd:[Ctrl-p], kbd:[Ctrl-a], kbd:[Ctrl-e]. By pressing kbd:[,] users can enter
16 | directories or open files on the screen using
17 | https://github.com/abo-abo/avy/[avy]. Pressing kbd:[;] will jump to the
18 | selected candidate and invoke the dipsatch action dialog. This works because
19 | those characters are rarely used for file search. If you have to input them
20 | you can still use kbd:[C-q ,], kbd:[C-q ;].
21 |
22 | Heavily inspired by
23 | https://www.emacswiki.org/emacs/LustyExplorer[LustyExplorer].
24 |
25 | = Demo
26 |
27 | .Ivy explorer grid
28 | image::./images/grid.gif[grid]
29 |
30 | You can also use https://github.com/tumashu/posframe[posframe] to display the
31 | grid by setting `ivy-explorer-message-function` to `ivy-explorer--posframe`.
32 | The height of the posframe can be set by `ivy-explorer-height`.
33 |
34 | .Ivy explorer grid using posframe
35 | image::./images/posframe.png[grid]
36 |
37 |
38 |
39 | ++++
40 |
41 | ++++
42 |
43 |
44 | = Setup
45 |
46 | `ivy-explorer` is on MELPA and GNU ELPA, for installation use:
47 |
48 | ```emacs
49 | M-x package-refresh-contents RET
50 | M-x package-install RET ivy-explorer RET
51 | ```
52 | For manual installation:
53 |
54 | ```sh
55 | git clone http://github.com/clemera/ivy-explorer.git
56 | ```
57 | Add this to your init file:
58 |
59 | ```elisp
60 | (add-to-list 'load-path "//ivy-explorer/")
61 | (require 'ivy-explorer)
62 | ;; use ivy explorer for all file dialogs
63 | (ivy-explorer-mode 1)
64 | ;; not strictly necessary
65 | (counsel-mode 1)
66 | ```
67 |
68 | = Contribute
69 |
70 | This package is subject to the same
71 | https://www.gnu.org/prep/maintain/html_node/Copyright-Papers.html[Copyright Assignment]
72 | policy as Emacs itself, org-mode, CEDET and other packages in https://elpa.gnu.org/packages/[GNU ELPA].
73 |
74 | Any
75 | https://www.gnu.org/prep/maintain/html_node/Legally-Significant.html#Legally-Significant[legally significant]
76 | contributions can only be accepted after the author has completed
77 | their paperwork. Please see
78 | https://git.savannah.gnu.org/cgit/gnulib.git/tree/doc/Copyright/request-assign.future[the request form]
79 | if you want to proceed with the assignment.
80 |
81 |
82 |
--------------------------------------------------------------------------------
/ivy-explorer.el:
--------------------------------------------------------------------------------
1 | ;;; ivy-explorer.el --- Dynamic file browsing grid using ivy -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2018-2019 Free Software Foundation, Inc.
4 |
5 | ;; Author: Clemens Radermacher
6 | ;; URL: https://github.com/clemera/ivy-explorer
7 | ;; Version: 0.3.2
8 | ;; Package-Requires: ((emacs "25") (ivy "0.10.0"))
9 | ;; Keywords: convenience, files, matching
10 |
11 | ;; This program is free software; you can redistribute it and/or modify
12 | ;; it under the terms of the GNU General Public License as published by
13 | ;; the Free Software Foundation, either version 3 of the License, or
14 | ;; (at your option) any later version.
15 |
16 | ;; This program is distributed in the hope that it will be useful,
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | ;; GNU General Public License for more details.
20 |
21 | ;; You should have received a copy of the GNU General Public License
22 | ;; along with this program. If not, see .
23 |
24 | ;;; Commentary:
25 | ;;
26 | ;; Provides a large more easily readable grid for file browsing using
27 | ;; `ivy'. When `avy' is installed, commands for fast avy navigation
28 | ;; are available to the user, as well. Heavily inspired by
29 | ;; LustyExplorer:
30 | ;;
31 | ;; https://www.emacswiki.org/emacs/LustyExplorer
32 | ;;
33 | ;; Known Bugs:
34 | ;;
35 | ;; When the number of candidates don't fit into the ivy-explorer
36 | ;; window, moving down along the grid can change the order of elements
37 | ;; when new candidates get displayed. This can change the column the
38 | ;; user is currently in while moving vertically down or up. Although
39 | ;; this is a bit confusing the correct candidate gets selected.
40 | ;; Patches welcome!
41 | ;;
42 | ;;; Code:
43 |
44 |
45 | (require 'ivy)
46 |
47 | (defgroup ivy-explorer nil
48 | "Dynamic file browsing grid using ivy."
49 | :group 'ivy
50 | :group 'files)
51 |
52 | (defcustom ivy-explorer-enable-counsel-explorer t
53 | "If non-nil remap `find-file' to `counsel-explorer'.
54 |
55 | This will also override remappings of function/`counsel-mode' for
56 | `find-file' (`counsel-find-file').
57 |
58 | This variable has to be (un)set before loading `ivy-explorer' to
59 | take effect."
60 | :group 'ivy-explorer
61 | :type 'boolean)
62 |
63 | (defcustom ivy-explorer-use-separator t
64 | "Whether to draw a line as separator.
65 |
66 | Line is drawn between the ivy explorer window and the Echo Area."
67 | :group 'ivy-explorer
68 | :type 'boolean)
69 |
70 | (defcustom ivy-explorer-max-columns 5
71 | "If given the maximal number of columns to use.
72 |
73 | If the grid does not fit on the screen the number of columns is
74 | adjusted to a lower number automatically."
75 | :group 'ivy-explorer
76 | :type 'integer)
77 |
78 | (defcustom ivy-explorer-width (frame-width)
79 | "Width used to display the grid."
80 | :type 'integer)
81 |
82 | (defcustom ivy-explorer-max-function #'ivy-explorer-max
83 | "Function which should return max number of canidates."
84 | :group 'ivy-explorer
85 | :type 'function)
86 |
87 | (defcustom ivy-explorer-message-function #'ivy-explorer--lv-message
88 | "Function to be used for grid display.
89 |
90 | By default you can choose between `ivy-explorer--posframe' and
91 | `ivy-explorer--lv-message'."
92 | :group 'ivy-explorer
93 | :type 'function)
94 |
95 | (defcustom ivy-explorer-height ivy-height
96 | "Height used if `ivy-explorer-message-function' has no dynamic height."
97 | :type 'integer)
98 |
99 | (defcustom ivy-explorer-auto-init-avy nil
100 | "Whether to load grid views with avy selection enabled by default."
101 | :group 'ivy-explorer
102 | :type 'boolean)
103 |
104 | (defcustom ivy-explorer-avy-handler-alist
105 | (list (cons #'ivy-explorer--lv-message
106 | #'ivy-explorer-avy-default-handler)
107 | (cons #'ivy-explorer--posframe
108 | #'ivy-explorer-avy-posframe-handler))
109 | "Alist which maps message functions to avy handlers.
110 |
111 | The message functions are the candidates for
112 | `ivy-explorer-message-function'. When avy selection command is
113 | invoked the corresponding handler gets used."
114 | :type '(alist :key-type function
115 | :value-type function))
116 |
117 | (defface ivy-explorer-separator
118 | (if (featurep 'lv)
119 | '((t (:inherit lv-separator)))
120 | '((t (:inherit border))))
121 | "Face used to draw line between the ivy-explorer window and the echo area.
122 | This is only used if option `ivy-explorer-use-separator' is non-nil.
123 | Only the background color is significant."
124 | :group 'ivy-explorer)
125 |
126 |
127 | (defvar ivy-explorer-map
128 | (let ((map (make-sparse-keymap)))
129 | (prog1 map
130 | (define-key map (kbd "DEL") 'ivy-explorer-backward-delete-char)
131 | (define-key map (kbd "C-j") 'ivy-explorer-alt-done)
132 | (define-key map (kbd "C-x d") 'ivy-explorer-dired)
133 |
134 | (define-key map (kbd "C-x o") 'ivy-explorer-other-window)
135 | (define-key map (kbd "'") 'ivy-explorer-other-window)
136 |
137 | (define-key map (kbd "M-o") 'ivy-explorer-dispatching-done)
138 | (define-key map (kbd "C-'") 'ivy-explorer-avy)
139 | (define-key map (kbd ",") 'ivy-explorer-avy)
140 | (define-key map (kbd ";") 'ivy-explorer-avy-dispatch)
141 | ;; TODO: create C-o ivy-explorer-hydra
142 | (define-key map (kbd "C-f") 'ivy-explorer-forward)
143 | (define-key map (kbd "C-b") 'ivy-explorer-backward)
144 | (define-key map (kbd "C-M-f") 'ivy-explorer-forward-and-call)
145 | (define-key map (kbd "C-M-b") 'ivy-explorer-backward-and-call)
146 |
147 | (define-key map (kbd "C-a") 'ivy-explorer-bol)
148 | (define-key map (kbd "C-e") 'ivy-explorer-eol)
149 | (define-key map (kbd "C-M-a") 'ivy-explorer-bol-and-call)
150 | (define-key map (kbd "C-M-e") 'ivy-explorer-eol-and-call)
151 |
152 | (define-key map (kbd "C-n") 'ivy-explorer-next)
153 | (define-key map (kbd "C-p") 'ivy-explorer-previous)
154 | (define-key map (kbd "C-M-n") 'ivy-explorer-next-and-call)
155 | (define-key map (kbd "C-M-p") 'ivy-explorer-previous-and-call)))
156 | "Keymap used in the minibuffer for function/`ivy-explorer-mode'.")
157 |
158 | ;; * Ivy settings
159 |
160 | (when (bound-and-true-p ivy-display-functions-props)
161 | (push '(ivy-explorer--display-function :cleanup ivy-explorer--cleanup)
162 | ivy-display-functions-props))
163 |
164 | (defvar ivy-explorer--posframe-buffer
165 | " *ivy-explorer-pos-frame-buffer*")
166 |
167 | (defun ivy-explorer--cleanup ()
168 | (when (and ivy-explorer-mode
169 | (eq ivy-explorer-message-function
170 | #'ivy-explorer--posframe)
171 | (string-match "posframe"
172 | (symbol-name ivy-explorer-message-function)))
173 | (posframe-hide ivy-explorer--posframe-buffer)))
174 |
175 | ;; * Ivy explorer menu
176 |
177 | (defvar ivy-explorer--col-n nil
178 | "Current columns size of grid.")
179 |
180 | (defvar ivy-explorer--row-n nil
181 | "Current row size of grid.")
182 |
183 | (defun ivy-explorer--get-menu-string (strings cols &optional width)
184 | "Given a list of STRINGS create a menu string.
185 |
186 | The menu string will be segmented into columns. COLS is the
187 | maximum number of columns to use. Decisions to use less number of
188 | columns is based on WIDTH which defaults to frame width. Returns
189 | a cons cell with the (columns . rows) created as the `car' and
190 | the menu string as `cdr'."
191 | (with-temp-buffer
192 | (let* ((length (apply 'max
193 | (mapcar #'string-width strings)))
194 | (wwidth (or width (frame-width)))
195 | (columns (min cols (/ wwidth (+ 2 length))))
196 | (rows 1)
197 | (colwidth (/ wwidth columns))
198 | (column 0)
199 | (first t)
200 | laststring)
201 | (dolist (str strings)
202 | (unless (equal laststring str)
203 | (setq laststring str)
204 | (let ((length (string-width str)))
205 | (unless first
206 | (if (or (< wwidth (+ (max colwidth length) column))
207 | (zerop length))
208 | (progn
209 | (cl-incf rows)
210 | (insert "\n" (if (zerop length) "\n" ""))
211 | (setq column 0))
212 | (insert " \t")
213 | (set-text-properties (1- (point)) (point)
214 | `(display (space :align-to ,column)))))
215 | (setq first (zerop length))
216 | (insert str)
217 | (setq column (+ column
218 | (* colwidth (ceiling length colwidth)))))))
219 | (cons (cons columns rows) (buffer-string)))))
220 |
221 | ;; * Ivy explorer window, adapted from lv.el
222 |
223 | (defvar display-line-numbers)
224 | (defvar golden-ratio-mode)
225 |
226 | (defvar ivy-explorer--window nil
227 | "Holds the current ivy explorer window.")
228 |
229 | (defmacro ivy-explorer--lv-command (cmd)
230 | `(defun ,(intern (format "%s-lv" (symbol-name cmd))) ()
231 | (interactive)
232 | (with-selected-window (minibuffer-window)
233 | (call-interactively ',cmd)
234 | (ivy--exhibit))))
235 |
236 | (defun ivy-explorer-select-mini ()
237 | (interactive)
238 | (select-window (minibuffer-window)))
239 |
240 | (defvar ivy-explorer-lv-mode-map
241 | (let ((map (make-sparse-keymap)))
242 | (prog1 map
243 | (suppress-keymap map)
244 | (define-key map (kbd "C-g") (defun ivy-explorer-lv-quit ()
245 | (interactive)
246 | (with-selected-window (minibuffer-window)
247 | (minibuffer-keyboard-quit))))
248 | (define-key map "n" (ivy-explorer--lv-command ivy-explorer-next))
249 | (define-key map "p" (ivy-explorer--lv-command ivy-explorer-previous))
250 | (define-key map "f" (ivy-explorer--lv-command ivy-explorer-forward))
251 | (define-key map "b" (ivy-explorer--lv-command ivy-explorer-backward))
252 | (define-key map (kbd "RET") (ivy-explorer--lv-command ivy-alt-done))
253 | (define-key map (kbd "DEL") (ivy-explorer--lv-command ivy-backward-delete-char))
254 | (define-key map (kbd "M-o") (ivy-explorer--lv-command ivy-explorer-dispatching-done))
255 | (define-key map "," (ivy-explorer--lv-command ivy-explorer-avy))
256 | (define-key map (kbd "C-x o") 'ivy-explorer-select-mini)
257 | (define-key map (kbd "'") 'ivy-explorer-select-mini))))
258 |
259 | (define-minor-mode ivy-explorer-lv-mode
260 | "Mode for buffer showing the grid.")
261 |
262 | (defun ivy-explorer--lv ()
263 | "Ensure that ivy explorer window is live and return it."
264 | (if (window-live-p ivy-explorer--window)
265 | ivy-explorer--window
266 | (let ((ori (selected-window))
267 | buf)
268 | (prog1 (setq ivy-explorer--window
269 | (select-window
270 | (let ((ignore-window-parameters t))
271 | (split-window
272 | (frame-root-window) -1 'below))))
273 | (if (setq buf (get-buffer " *ivy-explorer*"))
274 | (switch-to-buffer buf)
275 | (switch-to-buffer " *ivy-explorer*")
276 | (set-window-hscroll ivy-explorer--window 0)
277 | (ivy-explorer-lv-mode 1)
278 | (setq window-size-fixed t)
279 | (setq mode-line-format nil)
280 | (setq cursor-type nil)
281 | (setq display-line-numbers nil)
282 | (set-window-dedicated-p ivy-explorer--window t)
283 | (set-window-parameter ivy-explorer--window 'no-other-window t))
284 | (select-window ori)))))
285 |
286 | (defun ivy-explorer--lv-message (str)
287 | "Set ivy explorer window contents to string STR."
288 | (let* ((str (substring str 1))
289 | (n-lines (cl-count ?\n str))
290 | (window-size-fixed nil)
291 | deactivate-mark
292 | golden-ratio-mode)
293 | (with-selected-window (ivy-explorer--lv)
294 | (unless (string= (buffer-string) str)
295 | (delete-region (point-min) (point-max))
296 | (insert str)
297 | (when (and (window-system) ivy-explorer-use-separator)
298 | (unless (looking-back "\n" nil)
299 | (insert "\n"))
300 | (insert
301 | (propertize "__" 'face
302 | 'ivy-explorer-separator 'display '(space :height (1)))
303 | (propertize "\n" 'face
304 | 'ivy-explorer-separator 'line-height t)))
305 | (set (make-local-variable 'window-min-height) n-lines)
306 | (setq truncate-lines (> n-lines 1))
307 | (let ((window-resize-pixelwise t)
308 | (window-size-fixed nil))
309 | (fit-window-to-buffer nil nil 1)))
310 | (goto-char (point-min)))))
311 |
312 | (defun ivy-explorer--lv-delete-window ()
313 | "Delete ivy explorer window and kill its buffer."
314 | (when (window-live-p ivy-explorer--window)
315 | (let ((buf (window-buffer ivy-explorer--window)))
316 | (delete-window ivy-explorer--window)
317 | (kill-buffer buf))))
318 |
319 |
320 | (defun ivy-explorer--posframe (msg)
321 | (unless (require 'posframe nil t)
322 | (user-error "Posframe library not found"))
323 | (unless (bound-and-true-p ivy-display-functions-props)
324 | (user-error "Ivy version to old, use melpa version if possible"))
325 | (with-selected-window (ivy--get-window ivy-last)
326 | (posframe-show
327 | ivy-explorer--posframe-buffer
328 | :string
329 | (with-current-buffer (get-buffer-create " *Minibuf-1*")
330 | (let ((point (point))
331 | (string (concat (buffer-string) " " msg)))
332 | (add-text-properties (- point 1) point '(face (:inherit cursor))
333 | string)
334 | string))
335 | :poshandler (lambda (info)
336 | (cons (frame-parameter nil 'left-fringe)
337 | (- 0
338 | ;; TODO: calculate based on ivy-explorer-height
339 | (plist-get info :mode-line-height)
340 | (plist-get info :minibuffer-height))))
341 | :background-color (or (and (facep 'ivy-posframe)
342 | (face-attribute 'ivy-posframe :background))
343 | (face-attribute 'fringe :background))
344 | :foreground-color (or (and (facep 'ivy-posframe)
345 | (face-attribute 'ivy-posframe :foreground)
346 | (face-attribute 'default :foreground)))
347 | :internal-border-width (or (and (bound-and-true-p ivy-posframe-border-width)
348 | ivy-posframe-border-width)
349 | 0)
350 | :height ivy-explorer-height
351 | :left-fringe (frame-parameter nil 'left-fringe)
352 | :right-fringe (frame-parameter nil 'right-fringe)
353 | :width (frame-width))))
354 |
355 |
356 | ;; * Minibuffer commands
357 |
358 | (defun ivy-explorer--ace-handler (char)
359 | "Execute buffer-expose action for CHAR."
360 | (cond ((memq char '(27 ?\C-g ?,))
361 | ;; exit silently
362 | (throw 'done 'exit))
363 | ((mouse-event-p char)
364 | (signal 'user-error (list "Mouse event not handled" char)))
365 | (t
366 | (require 'edmacro)
367 | (let* ((key (kbd (edmacro-format-keys (vector char))))
368 | (cmd (or (lookup-key ivy-explorer-map key)
369 | (key-binding key))))
370 | (if (commandp cmd)
371 | (progn (call-interactively cmd)
372 | (run-at-time 0 nil #'ivy--exhibit)
373 | (throw 'done 'exit))
374 | (message "No such candidate: %s, hit `C-g' to quit."
375 | (if (characterp char) (string char) char))
376 | (throw 'done 'restart))))))
377 |
378 |
379 | (defvar avy-all-windows)
380 | (defvar avy-keys)
381 | (defvar avy-keys-alist)
382 | (defvar avy-style)
383 | (defvar avy-styles-alist)
384 | (defvar avy-action)
385 | (defun ivy-explorer--avy ()
386 | (let* ((avy-all-windows nil)
387 | (avy-keys (or (cdr (assq 'ivy-avy avy-keys-alist))
388 | avy-keys))
389 | (avy-handler-function #'ivy-explorer--ace-handler)
390 | (avy-pre-action #'ignore)
391 | (avy-style (or (cdr (assq 'ivy-avy
392 | avy-styles-alist))
393 | avy-style))
394 | (avy-action #'identity)
395 | (handler (cdr (assq ivy-explorer-message-function
396 | ivy-explorer-avy-handler-alist)))
397 | (success (if handler (funcall handler)
398 | (user-error "No handler for %s found in `ivy-explorer-avy-handler-alist'"
399 | ivy-explorer-message-function))))
400 | success))
401 |
402 | (defun ivy-explorer-avy-default-handler ()
403 | (let* ((w (ivy-explorer--lv))
404 | (b (window-buffer w)))
405 | (with-selected-window w
406 | (ivy-explorer--avy-1 b))))
407 |
408 | (defun ivy-explorer-avy-posframe-handler ()
409 | (let* ((b ivy-explorer--posframe-buffer)
410 | (w (frame-selected-window
411 | (buffer-local-value 'posframe--frame
412 | (get-buffer b)))))
413 | (with-selected-window w
414 | (ivy-explorer--avy-1 b (with-current-buffer b
415 | (save-excursion
416 | (goto-char (point-min))
417 | (forward-line 1)
418 | (point)))
419 | (window-end w)))))
420 |
421 | (defun ivy-explorer--avy-1 (&optional buffer start end)
422 | (let ((candidate (avy--process
423 | (ivy-explorer--parse-avy-buffer buffer start end)
424 | (avy--style-fn avy-style))))
425 | (when (number-or-marker-p candidate)
426 | (prog1 t
427 | (ivy-set-index
428 | (get-text-property candidate 'ivy-explorer-count))))))
429 |
430 | (defun ivy-explorer--parse-avy-buffer (&optional buffer start end)
431 | (let ((count 0)
432 | (candidates ())
433 | (start (or start (point-min)))
434 | (end (or end (point-max))))
435 | (with-current-buffer (or buffer (current-buffer))
436 | (save-excursion
437 | (save-restriction
438 | (narrow-to-region start end)
439 | (goto-char (point-min))
440 | ;; ignore the first candidate if at ./
441 | ;; this command is meant to be used for navigation
442 | ;; navigate to same folder you are in makes no sense
443 | (unless (search-forward "./" nil t)
444 | (push (cons (point)
445 | (selected-window))
446 | candidates)
447 | (put-text-property
448 | (point) (1+ (point)) 'ivy-explorer-count count))
449 | (goto-char
450 | (or (next-single-property-change
451 | (point) 'mouse-face)
452 | (point-max)))
453 | (while (< (point) (point-max))
454 | (unless (looking-at "[[:blank:]\r\n]\\|\\'")
455 | (cl-incf count)
456 | (put-text-property
457 | (point) (1+ (point)) 'ivy-explorer-count count)
458 | (push
459 | (cons (point)
460 | (selected-window))
461 | candidates))
462 | (goto-char
463 | (or (next-single-property-change
464 | (point)
465 | 'mouse-face)
466 | (point-max)))))))
467 | (nreverse candidates)))
468 |
469 |
470 | ;; Ivy explorer avy, adapted from ivy-avy
471 | (defun ivy-explorer-avy (&optional action)
472 | "Jump to one of the current candidates using `avy'.
473 |
474 | Files are opened and directories will be entered. When entering a
475 | directory `avy' is invoked again. Users can exit this navigation
476 | style with C-g.
477 |
478 | If called from code ACTION is the action to trigger afterwards,
479 | in this case `avy' is not invoked again."
480 | (interactive)
481 | (unless (require 'avy nil 'noerror)
482 | (error "Package avy isn't installed"))
483 | (when (ivy-explorer--avy)
484 | (ivy--exhibit)
485 | (funcall (or action #'ivy-alt-done))))
486 |
487 | ;; adapted from ivy-hydra
488 | (defun ivy-explorer-avy-dispatching-done-hydra ()
489 | "Choose action and afterwards target using `hydra'."
490 | (interactive)
491 | (let* ((actions (ivy-state-action ivy-last))
492 | (estimated-len (+ 25 (length
493 | (mapconcat
494 | (lambda (x)
495 | (format "[%s] %s" (nth 0 x) (nth 2 x)))
496 | (cdr actions) ", "))))
497 | (n-columns (if (> estimated-len (window-width))
498 | (or (and (bound-and-true-p ivy-dispatching-done-columns)
499 | ivy-dispatching-done-columns)
500 | 2)
501 | nil)))
502 | (if (null (ivy--actionp actions))
503 | (ivy-done)
504 | (ivy-explorer-avy 'ignore)
505 | (funcall
506 | (eval
507 | `(defhydra ivy-read-action (:color teal :columns ,n-columns)
508 | "action"
509 | ,@(mapcar (lambda (x)
510 | (list (nth 0 x)
511 | `(progn
512 | (ivy-set-action ',(nth 1 x))
513 | (ivy-done))
514 | (nth 2 x)))
515 | (cdr actions))
516 | ("M-i" nil "back")
517 | ("C-g" nil)))))))
518 |
519 | (defun ivy-explorer-avy-dispatch ()
520 | "Choose target with avy and afterwards dispatch action."
521 | (interactive)
522 | (setq ivy-current-prefix-arg current-prefix-arg)
523 | (if (require 'hydra nil t)
524 | (call-interactively
525 | 'ivy-explorer-avy-dispatching-done-hydra)
526 | (ivy-explorer-avy
527 | (lambda ()
528 | (let ((action (if (get-buffer ivy-explorer--posframe-buffer)
529 | (progn (unless (require 'ivy-posframe nil t)
530 | (user-error "Ivy posframe not found"))
531 | (ivy-posframe-read-action))
532 | (ivy-read-action))))
533 | (when action
534 | (ivy-set-action action)
535 | (ivy-done)))))))
536 |
537 | (declare-function dired-goto-file "ext:dired")
538 | (defun ivy-explorer-dired ()
539 | "Open current directory in `dired'.
540 |
541 | Move to file which was current on exit."
542 | (interactive)
543 | (let ((curr (ivy-state-current ivy-last)))
544 | (ivy--cd ivy--directory)
545 | (ivy--exhibit)
546 | (run-at-time 0 nil #'dired-goto-file
547 | (expand-file-name curr ivy--directory))
548 | (ivy-done)))
549 |
550 | (defun ivy-explorer-next (arg)
551 | "Move cursor vertically down ARG candidates."
552 | (interactive "p")
553 | (if (> (minibuffer-depth) 1)
554 | (call-interactively 'ivy-next-line)
555 | (let* ((n (* arg ivy-explorer--col-n))
556 | (max (1- ivy--length))
557 | (colmax (- max (% (- max ivy--index) n))))
558 | (ivy-set-index
559 | (if (= ivy--index -1)
560 | 0
561 | (min colmax
562 | (+ ivy--index n)))))))
563 |
564 | (defun ivy-explorer-eol ()
565 | "Move cursor to last column."
566 | (interactive)
567 | (let ((ci (% ivy--index ivy-explorer--col-n)))
568 | (ivy-explorer-forward (- (1- ivy-explorer--col-n) ci))))
569 |
570 | (defun ivy-explorer-eol-and-call ()
571 | "Move cursor to last column.
572 |
573 | Call the permanent action if possible."
574 | (interactive)
575 | (ivy-explorer-eol)
576 | (ivy--exhibit)
577 | (ivy-call))
578 |
579 | (defun ivy-explorer-bol ()
580 | "Move cursor to first column."
581 | (interactive)
582 | (let ((ci (% ivy--index ivy-explorer--col-n)))
583 | (ivy-explorer-backward ci)))
584 |
585 | (defun ivy-explorer-bol-and-call ()
586 | "Move cursor to first column.
587 |
588 | Call the permanent action if possible."
589 | (interactive)
590 | (ivy-explorer-bol)
591 | (ivy--exhibit)
592 | (ivy-call))
593 |
594 | (defun ivy-explorer-next-and-call (arg)
595 | "Move cursor down ARG candidates.
596 |
597 | Call the permanent action if possible."
598 | (interactive "p")
599 | (ivy-explorer-next arg)
600 | (ivy--exhibit)
601 | (ivy-call))
602 |
603 | (defun ivy-explorer-previous (arg)
604 | "Move cursor vertically up ARG candidates."
605 | (interactive "p")
606 | (if (> (minibuffer-depth) 1)
607 | (call-interactively 'ivy-previous-line)
608 | (let* ((n (* arg ivy-explorer--col-n))
609 | (colmin (% ivy--index n)))
610 | (ivy-set-index
611 | (if (and (= ivy--index 0)
612 | ivy-use-selectable-prompt)
613 | -1
614 | (max colmin
615 | (- ivy--index n)))))))
616 |
617 | (defun ivy-explorer-previous-and-call (arg)
618 | "Move cursor up ARG candidates.
619 | Call the permanent action if possible."
620 | (interactive "p")
621 | (ivy-explorer-previous arg)
622 | (ivy--exhibit)
623 | (ivy-call))
624 |
625 | (defalias 'ivy-explorer-forward #'ivy-next-line
626 | "Move cursor forward ARG candidates.")
627 |
628 | (defalias 'ivy-explorer-backward #'ivy-previous-line
629 | "Move cursor backward ARG candidates.")
630 |
631 | (defun ivy-explorer-alt-done ()
632 | "Like `ivy-alt-done' but respecting `ivy-explorer-auto-init-avy'."
633 | (interactive)
634 | (with-selected-window (minibuffer-window)
635 | (call-interactively 'ivy-alt-done)
636 | (when ivy-explorer-auto-init-avy
637 | (ivy-explorer-avy))))
638 |
639 | (defun ivy-explorer-backward-delete-char ()
640 | "Like `ivy-backward-delete-char' but respecting `ivy-explorer-auto-init-avy'."
641 | (interactive)
642 | (with-selected-window (minibuffer-window)
643 | (if (and ivy--directory (= (minibuffer-prompt-end) (point)))
644 | (progn (call-interactively 'ivy-backward-delete-char)
645 | (when ivy-explorer-auto-init-avy
646 | (ivy-explorer-avy)))
647 | (call-interactively 'ivy-backward-delete-char))))
648 |
649 |
650 | (defalias 'ivy-explorer-forward-and-call #'ivy-next-line-and-call
651 | "Move cursor forward ARG candidates.
652 | Call the permanent action if possible.")
653 |
654 | (defalias 'ivy-explorer-backward-and-call #'ivy-previous-line-and-call
655 | "Move cursor backward ARG candidates.
656 | Call the permanent action if possible.")
657 |
658 | ;; * Ivy explorer mode
659 |
660 | (defun ivy-explorer-other-window ()
661 | (interactive)
662 | (let ((w (or (get-buffer-window " *ivy-explorer*")
663 | (get-buffer-window (ivy-state-buffer ivy-last)))))
664 | (when (window-live-p w)
665 | (select-window w))))
666 |
667 | (defun ivy-explorer-max ()
668 | "Default for `ivy-explorer-max-function'."
669 | (* 2 (frame-height)))
670 |
671 | (defun ivy-explorer--display-function (text)
672 | "Displays TEXT as `ivy-display-function'."
673 | (let* ((strings (or (split-string text "\n" t)
674 | (list "")))
675 | (menu (ivy-explorer--get-menu-string
676 | strings ivy-explorer-max-columns ivy-explorer-width))
677 | (mcols (caar menu))
678 | (mrows (cdar menu))
679 | (mstring (cdr menu)))
680 | (setq ivy-explorer--col-n mcols)
681 | (setq ivy-explorer--row-n mrows)
682 | (funcall ivy-explorer-message-function (concat "\n" mstring))))
683 |
684 |
685 | (defun ivy-explorer-read (prompt coll &optional avy msgf mcols width height)
686 | "Read value from an explorer grid.
687 |
688 | PROMPT and COLL are the same as for `ivy-read'. If AVY is non-nil
689 | the grid is initilized with avy selection.
690 |
691 | MCOLS is the number of columns to use. If the grid does not fit
692 | on the screen the number of columns is adjusted to a lower number
693 | automatically. If not given the the value is calculated
694 | by (/ (frame-width) 30)
695 |
696 | WIDTH is the width to be used to create the grid and defaults to
697 | frame-width.
698 |
699 | Height is the height for the grid display and defaults to
700 | ivy-height.
701 |
702 | MSGF is the function to be called with the grid string and defaults to
703 | `ivy-explorer-message-function.'"
704 | (let ((ivy-explorer-message-function (or msgf ivy-explorer-message-function))
705 | (ivy-explorer-max-columns (or mcols (/ (frame-width) 30)))
706 | (ivy-wrap nil)
707 | (ivy-explorer-height (or height ivy-explorer-height))
708 | (ivy-explorer-width (or width (frame-width)))
709 | (ivy-height (funcall ivy-explorer-max-function))
710 | (ivy-display-function #'ivy-explorer--display-function)
711 | (ivy-display-functions-alist '((t . ivy-explorer--display-function)))
712 | (ivy-posframe-display-functions-alist nil)
713 | (ivy-posframe-hide-minibuffer
714 | (eq ivy-explorer-message-function #'ivy-explorer--posframe))
715 | (ivy-posframe--display-p t)
716 | (ivy-minibuffer-map (make-composed-keymap
717 | ivy-explorer-map ivy-minibuffer-map)))
718 | (when avy
719 | (run-at-time 0 nil 'ivy-explorer-avy))
720 | (ivy-read prompt coll)))
721 |
722 |
723 | (defun ivy-explorer--internal (f &rest args)
724 | "Invoke ivy explorer for F with ARGS."
725 | (let ((ivy-display-function #'ivy-explorer--display-function)
726 | (ivy-display-functions-alist '((t . ivy-explorer--display-function)))
727 | (ivy-posframe-display-functions-alist nil)
728 | (completing-read-function 'ivy-completing-read)
729 | (ivy-posframe-hide-minibuffer
730 | (eq ivy-explorer-message-function #'ivy-explorer--posframe))
731 | (ivy-posframe--display-p t)
732 | ;; max number of candidates
733 | (ivy-height (funcall ivy-explorer-max-function))
734 | (ivy-wrap nil)
735 | (ivy-minibuffer-map (make-composed-keymap
736 | ivy-explorer-map ivy-minibuffer-map)))
737 | (when ivy-explorer-auto-init-avy
738 | (run-at-time 0 nil 'ivy-explorer-avy))
739 | (apply f args)))
740 |
741 |
742 | (defun ivy-explorer-dispatching-done ()
743 | "Select one of the available actions and call `ivy-done'."
744 | (interactive)
745 | (cond ((get-buffer ivy-explorer--posframe-buffer)
746 | (unless (require 'ivy-posframe nil t)
747 | (user-error "Ivy posframe not found"))
748 | (ivy-posframe-dispatching-done))
749 | (t
750 | (let ((window (selected-window)))
751 | (unwind-protect
752 | (when (ivy-read-action)
753 | (ivy-done))
754 | (when (window-live-p window)
755 | (window-resize nil (- 1 (window-height)))))))))
756 |
757 |
758 | (defun ivy-explorer (&rest args)
759 | "Function to be used as `read-file-name-function'.
760 |
761 | ARGS are bassed to `read-file-name-default'."
762 | (apply #'ivy-explorer--internal #'read-file-name-default args))
763 |
764 |
765 | (defun counsel-explorer (&optional initial-input)
766 | "`counsel-find-file' version for ivy explorer.
767 |
768 | INITIAL-INPUT is passed to `counsel-find-file'."
769 | (interactive)
770 | (apply #'ivy-explorer--internal #'counsel-find-file initial-input))
771 |
772 | (defvar ivy-explorer-mode-map
773 | (let ((map (make-sparse-keymap)))
774 | (prog1 map
775 | (when ivy-explorer-enable-counsel-explorer
776 | (define-key map
777 | [remap find-file] #'counsel-explorer))))
778 | "Keymap for function/`ivy-explorer-mode'.")
779 |
780 | ;; from lispy.el
781 | (defun ivy-explorer-raise ()
782 | "Make function/`ivy-explorer-mode' the first on `minor-mode-map-alist'."
783 | (let ((x (assq #'ivy-explorer-mode minor-mode-map-alist)))
784 | (when x
785 | (setq minor-mode-map-alist
786 | (cons x
787 | (delq #'ivy-explorer-mode minor-mode-map-alist))))))
788 |
789 | (defvar ivy-explorer--default nil
790 | "Saves user configured `read-file-name-function'.")
791 |
792 |
793 | ;;;###autoload
794 | (define-minor-mode ivy-explorer-mode
795 | "Globally enable `ivy-explorer' for file navigation.
796 |
797 | `ivy-explorer-mode' is a global minor mode which changes
798 | `read-file-name-function' which is used for file completion.
799 |
800 | When `ivy-explorer-enable-counsel-explorer' (by default it is),
801 | `find-file' and `counsel-find-file' will be remapped to
802 | `counsel-explorer.', too.
803 |
804 | See `ivy-explorer-map' for bindings used in the minibuffer."
805 | :group 'ivy-explorer
806 | :require 'ivy-explorer
807 | :global t
808 | :init-value nil
809 | :lighter " ivy-explorer"
810 | (if (not ivy-explorer-mode)
811 | (setq read-file-name-function ivy-explorer--default)
812 | (setq ivy-explorer--default read-file-name-function
813 | read-file-name-function #'ivy-explorer)
814 | (when ivy-explorer-enable-counsel-explorer
815 | ;; in case users activate counsel afterwards.
816 | (add-hook 'counsel-mode-hook 'ivy-explorer-raise))))
817 |
818 | (provide 'ivy-explorer)
819 | ;;; ivy-explorer.el ends here
820 |
821 |
822 |
823 |
824 |
825 |
--------------------------------------------------------------------------------