EshellPrompt

Introduction

The Eshell prompt is generated by the function stored in ‘eshell-prompt-function’. When moving through the buffer, eshell also needs to know which lines start with a prompt. Therefore, whatever ‘eshell-prompt-function’ prints must be matched by ‘eshell-prompt-regexp’.

Here is a simple example:

(setq eshell-prompt-function
  (lambda ()
    (concat (format-time-string "%Y-%m-%d %H:%M" (current-time))
      (if (= (user-uid) 0) " # " " $ "))))

The resulting prompt:

2006-01-10 11:03 $ 
 (setq eshell-prompt-function
  (lambda nil
    (concat
     (eshell/pwd)
     " $ ")))

The resulting prompt:

 c:/home $ 

A colored prompt

If you want colors in your prompt, use something like this:

  (setq eshell-prompt-function (lambda nil
    (concat
     (propertize (eshell/pwd) 'face `(:foreground "blue"))
     (propertize " $ " 'face `(:foreground "green")))))
  (setq eshell-highlight-prompt nil)

A fancy prompt

Example of a more fancy eshell prompt with different colors, time, pwd, version control info and horizontal line after each command.

  (defun with-face (str &rest face-plist)
    (propertize str 'face face-plist))
  
  (defun shk-eshell-prompt ()
    (let ((header-bg "#fff"))
      (concat
       (with-face (concat (eshell/pwd) " ") :background header-bg)
       (with-face (format-time-string "(%Y-%m-%d %H:%M) " (current-time)) :background header-bg :foreground "#888")
       (with-face
        (or (ignore-errors (format "(%s)" (vc-responsible-backend default-directory))) "")
        :background header-bg)
       (with-face "\n" :background header-bg)
       (with-face user-login-name :foreground "blue")
       "@"
       (with-face "localhost" :foreground "green")
       (if (= (user-uid) 0)
           (with-face " #" :foreground "red")
         " $")
       " ")))
  (setq eshell-prompt-function 'shk-eshell-prompt)
  (setq eshell-highlight-prompt nil)

Summarising the path

I’ve always rather liked the fish shell’s habit of squashing path components down to their first character when your working directory gets too long, so wrote this function. If I were being really clever, I’d make sure that the names never became ambiguous (so if “baz” and “buz” both exist in “/foo/bar”, then we’d shorten to “ba” or “bu”, respectively). But I’m not feeling that clever and haven’t been burned by that ambiguity yet…

    (defun shortened-path (path max-len)
      "Return a modified version of `path', replacing some components
      with single characters starting from the left to try and get
      the path down to `max-len'"
      (let* ((components (split-string (abbreviate-file-name path) "/"))
             (len (+ (1- (length components))
                     (reduce '+ components :key 'length)))
             (str ""))
        (while (and (> len max-len)
                    (cdr components))
          (setq str (concat str (if (= 0 (length (car components)))
                                    "/"
                                  (string (elt (car components) 0) ?/)))
                len (- len (1- (length (car components))))
                components (cdr components)))
        (concat str (reduce (lambda (a b) (concat a "/" b)) components))))
    (defun rjs-eshell-prompt-function ()
      (concat (shortened-path (eshell/pwd) 40)
              (if (= (user-uid) 0) " # " " $ ")))

What it looks like:

~ $ mkdir -p some/ridiculously/long/directory/tree/which/is/too/deep/to/be/shown
~ $ cd some/ridiculously/long/directory/tree/which/is/too/deep/to/be/shown/
~/s/r/l/d/t/w/is/too/deep/to/be/shown $

With hidden directory abbreviations

The above ‘shortened-path’ function replaces hidden directories with a single dot, so “~/.emacs.d/lisp” becomes “~/./lisp”. In Fish, hidden parent directories are replaced with a dot plus the first non-dot character of the directory name, so “~/.emacs.d/lisp” would become “~/.e/lisp”. To get this behavior in Eshell, you can use the following function, only slightly modified from the above ‘shortened-path’:

(defun fish-path (path max-len)
  "Return a potentially trimmed-down version of the directory PATH, replacing
parent directories with their initial characters to try to get the character
length of PATH (sans directory slashes) down to MAX-LEN."
  (let* ((components (split-string (abbreviate-file-name path) "/"))
         (len (+ (1- (length components))
                 (reduce '+ components :key 'length)))
         (str ""))
    (while (and (> len max-len)
                (cdr components))
      (setq str (concat str
                        (cond ((= 0 (length (car components))) "/")
                              ((= 1 (length (car components)))
                               (concat (car components) "/"))
                              (t
                               (if (string= "."
                                            (string (elt (car components) 0)))
                                   (concat (substring (car components) 0 2)
                                           "/")
                                 (string (elt (car components) 0) ?/)))))
            len (- len (1- (length (car components))))
            components (cdr components)))
    (concat str (reduce (lambda (a b) (concat a "/" b)) components))))

Making the eshell prompt behave more like Bash/ZSH

There are a few features that I’ve become accustomed to that I’ve integrated into my own eshell prompt:

Below is a snippit from my configuration that attempts to accomplish these tasks:

(setq eshell-prompt-regexp "^[^#$\n]*[#$] "
      eshell-prompt-function
      (lambda nil
        (concat
	 "[" (user-login-name) "@" (system-name) " "
	 (if (string= (eshell/pwd) (getenv "HOME"))
	     "~" (eshell/basename (eshell/pwd)))
	 "]"
	 (if (= (user-uid) 0) "# " "$ "))))

CategoryEshell