For the life of me, I cannot get this functionality to work:


I've run (add-hook 'shell-mode-hook 'compilation-shell-minor-mode) in my .emacs file as the only line, hoping that it would allow me to click (or otherwise navigate) on a file path while in the shell and have that file opened, as is suggested in the link.

I hoped that this would work both for output of programs like stack traces or even things like symlinks when doing ls -l.

  • 1
    Can you write what you've tried, and what you want to achieve? – user12563 Jun 30 '19 at 10:50
  • 1
    It works for output with very specific format such as filename:line:column:, some compilers use the format to report errors, you can also try grep -nH or simply echo /path/to/file:10:5:. With some Emacs Lisp experience, it's not hard to make the output of ls clickable, though it does not worth if you use Dired. – xuchunyang Jun 30 '19 at 12:22
  • Agreed that ls isn't the most useful example, but debug or log output is great. Do you have any idea if there's a hook or configuration of some kind that allows one to tweak the file format? If you make an answer out of this comment I will accept it, and I also urge you to add this to the wiki for posterity. – skepticscript Jun 30 '19 at 15:44

The Elisp code below makes ls output of eshell clickable. It is basing on a method proposed on emacswiki.

If you click the mouse-button-2 (e.g., the middle mouse button/scroll-wheel) you get a pop up menu offering you to find the file in Emacs, to copy the file path, or to copy the path of the containing directory.

(require 'em-ls)
(require 'easymenu)

(defun eshellTZA-set-face-attribute (face-re &rest args)
  "Apply `set-face-attribute' to all faces that match FACE-RE."
  (cl-loop for face in (face-list)
           if (string-match face-re (face-name face))
           (apply #'set-face-attribute face nil args)))

(defface eshell-ls-normal '((((class color) (background light)) (:background "Light Yellow"))
    (((class color) (background dark)) (:background "Dark Yellow")))
  "The face used for highlighting normal files."
  :group 'eshell-ls)

(eshellTZA-set-face-attribute "^eshell-ls-" :background "Light Yellow")

(defvar-local eshellTZA-ls-dir nil
  "The eshell command `eshell/ls' calls indirectly `eshell-ls-dir' for each directory it lists.
The :filter-args advice `eshellTZA-ls-dir' of `eshell-ls-dir' sets this variable
to the absolute path of the directory that is actually listed
for each arg of `eshell/ls' that is an directory.")

(defun eshellTZA-ls-dir (args)
  "Filter-args advice for `eshell-ls-dir' with ARGS.
Set the variable `eshellTZA-ls-dir' to the directory listed next."
  (let ((dir (car-safe (car args)))
        (insert-name (nth 1 args))
        (root-dir default-directory))
    (setq eshellTZA-ls-dir (or (and (null insert-name)
                                    (stringp dir)
                                    (expand-file-name dir root-dir))

(advice-add #'eshell-ls-dir :filter-args #'eshellTZA-ls-dir)

(defvar eshellTZA-file-menu nil
  "Menu for <down-mouse-2> on ls-entries in eshell buffers.")

(setq eshellTZA-file-menu
      (let ((map (make-sparse-keymap "File Menu")))
        (easy-menu-add-item map nil ["Find File" find-file :keys ""])
        (easy-menu-add-item map nil ["Copy Absolute Path" kill-new :keys ""])
        (easy-menu-add-item map nil ["Copy Containing Dir" (lambda (f) (kill-new (file-name-directory f))) :keys ""])
        map)) ;

(defvar eshellTZA-ls-keymap nil
  "Value of the text property 'keymap for file names printed by eshell/ls.")

(setq eshellTZA-ls-keymap
      (let ((map (make-sparse-keymap)))
        (define-key map (kbd "RET")      'eshellTZA-ls-open-menu-at-point)
        (define-key map (kbd "<return>") 'eshellTZA-ls-open-menu-at-point)
        (define-key map (kbd "<mouse-2>") 'eshellTZA-ls-open-menu-at-mouse)

(defun eshellTZA-ls-open-menu-at-point (point)
  "Press return on Eshell's `ls' output to open `eshellTZA-file-menu'."
  (interactive "d")
  (eshellTZA-ls-open-menu point))

(defun eshellTZA-ls-open-menu-at-mouse (event)
  "Middle click on Eshell's `ls' output to open `eshellTZA-file-menu'.
Corresponding to ideas from https://www.emacswiki.org/emacs/EshellEnhancedLS"
  (interactive "e")
  (eshellTZA-ls-open-menu event))

(defun eshellTZA-ls-open-menu (&optional event-or-pos)
  "Middle click on Eshell's `ls' output to open `eshellTZA-file-menu'.
EVENT-OR-POS can be a mouse event or a point position.
Corresponding to ideas from https://www.emacswiki.org/emacs/EshellEnhancedLS"
  (unless event-or-pos
    (setq event-or-pos (point)))
  (let* ((event-posn (and (mouse-event-p event-or-pos) (event-end event-or-pos)))
     (point (if event-posn
              (select-window (posn-window event-posn))
              (posn-point event-posn))
     (position (window-absolute-pixel-position point))
     (position (if (mouse-event-p event-or-pos)
             (list (list (car position) (cdr position)) (selected-window))))
     (props (text-properties-at point))
     (fname (plist-get props 'eshellTZA-ls-file))
     (dir (plist-get props 'eshellTZA-ls-dir))
     (menu-item (x-popup-menu position eshellTZA-file-menu))
     (fun (lookup-key eshellTZA-file-menu (apply 'vector menu-item))))
    (when (and menu-item (stringp fname))
       ((functionp fun)
    (funcall fun (expand-file-name fname dir)))

(defun eshellTZA-ls-decorate (filename)
  "Advice for `eshell-ls-decorate' that adds
a popup menu to the string FNAME."
  (add-text-properties 0 (length filename)
                       (list 'help-echo "mouse-2: File Menu"
                             'mouse-face 'highlight
                             'keymap eshellTZA-ls-keymap
                             'eshellTZA-ls-file filename
                             'eshellTZA-ls-dir eshellTZA-ls-dir
  (unless (get-text-property 0 'face filename)
    (add-text-properties 0 (length filename)
                         (list 'face 'eshell-ls-normal)

(advice-add #'eshell-ls-decorated-name :filter-return #'eshellTZA-ls-decorate)

I've tested it with Emacs 26.2 running emacs -Q.

For testing you can copy-paste the code into your scratch buffer and evaluate it with M-x eval-buffer.

If you like the result you can put the code into your init file.

  • Thanks for sharing this code, it looks super useful. I'm going to work through it. @xuchunyang's comment is a little more related to my question since it answers what I can expect from Shell mode though. – skepticscript Jun 30 '19 at 15:48
  • +1, including for sharing the Emacs Wiki link. – Drew Jun 30 '19 at 20:52
  • 1
    @skepticscript Note, that this is about 14% of my eshell setup (counted in lines). I had to remove some treatment of special cases to make it work and I did not have much time to do that. So, if you recognize some strange stuff in it or if something does not work as expected just inform me. – Tobias Jun 30 '19 at 21:34
  • The colors "Light Yellow" and "Dark Yellow" are not valid color specs on Emacs built --with-ns. See M-x list-colors-display for available choices. On the one hand, this helped me locate a bug in a patch I'm working on that analyzes the color attribute to convert it to another form; but, on the other hand, other functions in Emacs C or Lisp may also have problems ... – lawlist Dec 18 '20 at 19:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.