├── .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 | ELPA 7 | MELPA Stable 8 | MELPA 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 | --------------------------------------------------------------------------------