Extending Eshell: Fuzzy History Search

December 17, 2020

I’ve been spending some of my time off customizing my Emacs config. I’ve used Emacs a long time but I’ve never dipped into writing Emacs Lisp to customize it. Now is the time.

I started trying to use the Emacs shell for various things (previous blog post), but one of the things I didn’t like was the history completion. I wanted my Ctrl-r to act like fzf rather than built-in commands that just pull the first thing that prefix-matches. It’s nice to see a list of what else is in the history and to have some sort of fuzzy matching capability.

It took about 10 minutes to patch together something searching the internet and M-x describe-function to find other relevant code inside Emacs itself. Here is what I came up with:

(defun timmy/counsel-eshell-history-action (cmd)
  "Insert cmd into the buffer"
  (interactive)
  (insert cmd))

(defun timmy/counsel-eshell-history (&optional initial-input)
  "Find command from eshell history.
INITIAL-INPUT can be given as the initial minibuffer input."
  (interactive)
    (ivy-read "Find cmd: " (timmy/eshell-history-list)
              :initial-input initial-input
              :action #'timmy/counsel-eshell-history-action
              :caller 'timmy/counsel-eshell-history))

(defun timmy/eshell-history-list ()
  "return the eshell history as a list"
  (and (or (not (ring-p eshell-history-ring))
	   (ring-empty-p eshell-history-ring))
       (error "No history"))
  (let* ((index (1- (ring-length eshell-history-ring)))
	 (ref (- (ring-length eshell-history-ring) index))
	 (items (list)))
    (while (>= index 0)
      (setq items (cons (format "%s" (eshell-get-history index)) items)
	    index (1- index)
	    ref (1+ ref)))
    items))

(use-package esh-mode
  :ensure nil
  :bind (:map eshell-mode-map
	      ("C-r" . timmy/counsel-eshell-history)))

I use counsel/ivy already, so I basically just wrote a function to gather the eshell history and pass it to counsel. Selecting the command is pretty straightforward–it just inserts the text into the buffer. There are probably a lot of better ways to write this code, but that’s not the point.

On a somewhat related note, I’m also enjoying setting up global bindings for my function keys. Here is what I have as of right now, but this will probably change a lot as I get a feel for what I like:

(global-set-key (kbd "<f1>") 'eshell)
(global-set-key (kbd "<f2>") 'recompile)
(global-set-key (kbd "<f5>") 'deadgrep)
(global-set-key (kbd "<f6>") 'counsel-git)
(global-set-key (kbd "<f9>") 'dired)
(global-set-key (kbd "<f10>") 'magit-status)
(global-set-key (kbd "<f11>") 'bury-buffer)
(global-set-key (kbd "<f12>") 'delete-other-windows)