334c3f9cff
Simple addition of some rings to store a bit of eshell command history. I really can't believe how easy this stuff is to write.
772 lines
36 KiB
Org Mode
772 lines
36 KiB
Org Mode
#+TITLE: Eshell
|
||
#+AUTHOR: Howard X. Abrams
|
||
#+DATE: 2022-09-13
|
||
#+FILETAGS: :emacs:
|
||
|
||
A literate programming file for configuring the Emacs Shell.
|
||
#+begin_src emacs-lisp :exports none
|
||
;;; ha-eshell --- Emacs Shell configuration. -*- lexical-binding: t; -*-
|
||
;;
|
||
;; © 2022 Howard X. Abrams
|
||
;; This work is licensed under a Creative Commons Attribution 4.0 International License.
|
||
;; See http://creativecommons.org/licenses/by/4.0/
|
||
;;
|
||
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
|
||
;; Maintainer: Howard X. Abrams
|
||
;; Created: September 13, 2022
|
||
;;
|
||
;; While obvious, GNU Emacs does not include this file or project.
|
||
;;
|
||
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
|
||
;; /Users/howard.abrams/other/hamacs/ha-eshell.org
|
||
;; And tangle the file to recreate this one.
|
||
;;
|
||
;;; Code:
|
||
#+end_src
|
||
* Introduction
|
||
While I like [[https://github.com/akermu/emacs-libvterm][vterm]], especially for logging into [[file:ha-remoting.org][remote systems]], I find Emacs’ shell, =eshell=, an interesting alternative.
|
||
If you find the documentation lacking, I [[http://www.howardism.org/Technical/Emacs/eshell-fun.html][documented most features]], and you might find the following helpful.
|
||
** Navigation and Keys
|
||
Along with the regular Emacs keybindings, Eshell comes with some interesting features:
|
||
- ~M-RET~ can be used to accumulate further commands while a command is currently running. Since all input is passed to the subprocess being executed, there is no automatic input queueing as there is with other shells.
|
||
- ~C-c C-t~ can be used to truncate the buffer if it grows too large.
|
||
- ~C-c C-r~ will move point to the beginning of the output of the last command. With a prefix argument, it will narrow to view only that output.
|
||
- ~C-c C-o~ will delete the output from the last command.
|
||
- ~C-c C-f~ will move forward a complete shell argument.
|
||
- ~C-c C-b~ will move backward a complete shell argument.
|
||
** Control-D Double Duty
|
||
Used to ~C-d~ exiting from a shell? Want it to keep working, but still allow deleting a character? We can have it both (thanks to [[https://github.com/wasamasa/dotemacs/blob/master/init.org#eshell][wasamasa]]):
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-quit-or-delete-char (arg)
|
||
"The `C-d' sequence closes window or deletes a character."
|
||
(interactive "p")
|
||
(if (and (eolp) (looking-back eshell-prompt-regexp))
|
||
(progn
|
||
(eshell-life-is-too-much) ; Why not? (eshell/exit)
|
||
(ignore-errors
|
||
(delete-window)))
|
||
(delete-forward-char arg)))
|
||
#+END_SRC
|
||
** Pager Setup
|
||
If any program wants to pause the output through the =$PAGER= variable, well, we don't really need that:
|
||
#+begin_src emacs-lisp
|
||
(setenv "PAGER" "cat")
|
||
#+end_src
|
||
* Aliases
|
||
Gotta have some [[http://www.emacswiki.org/emacs/EshellAlias][shell aliases]], right? We have three ways of doing that. First, enter them into an =eshell= session:
|
||
#+begin_src sh
|
||
alias less 'view-file $1'
|
||
#+end_src
|
||
Note that you need single quotes, and multiple arguments don’t really work with aliases.
|
||
|
||
Second, you can create/populate the alias file, =~/.emacs.d/eshell/alias= … as long as you don’t use those single quotes:
|
||
#+begin_src shell :tangle ~/.emacs.d/eshell/alias
|
||
alias ll ls -l $*
|
||
alias clear recenter 0
|
||
alias d dired $1
|
||
alias e find-file $1
|
||
alias less view-file $1
|
||
alias more view-file $1
|
||
alias find echo 'Please use fd instead.'
|
||
#+end_src
|
||
Which happens when you type those commands into an =eshell=.
|
||
|
||
Third, you want /control/, write a function to define the aliases:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun ha-eshell-add-aliases ()
|
||
"Call `eshell/alias' to define my aliases."
|
||
(eshell/alias "e" "find-file $1")
|
||
(eshell/alias "d" "dired $1")
|
||
(eshell/alias "gd" "magit-diff-unstaged")
|
||
(eshell/alias "gds" "magit-diff-staged")
|
||
|
||
;; The 'ls' executable requires the Gnu version on the Mac
|
||
(let ((ls (if (file-exists-p "/usr/local/bin/gls")
|
||
"/usr/local/bin/gls"
|
||
"/bin/ls")))
|
||
(eshell/alias "ll" (concat ls " -AlohG --color=always"))))
|
||
#+end_src
|
||
* Predicate Filters and Modifiers
|
||
The =T= predicate filter allows me to limit file results that have have internal =org-mode= tags. For instance, files that have a =#+TAGS:= header with a =mac= label will be given to the =grep= function:
|
||
#+begin_src sh
|
||
$ grep brew *.org(T'mac')
|
||
#+end_src
|
||
|
||
As described in [[http://www.howardism.org/Technical/Emacs/eshell-fun.html][this essay]], to extend Eshell, we need a two-part function:
|
||
1. Parse the Eshell buffer to look for the parameter (and move the point past the parameter).
|
||
2. A predicate function that takes a file as a parameter.
|
||
|
||
For the first step, we have our function /called/ as it helps parse the text at this time. Based on what it sees, it returns the predicate function used to filter the files:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-org-file-tags ()
|
||
"Helps the eshell parse the text the point is currently on,
|
||
looking for parameters surrounded in single quotes. Returns a
|
||
function that takes a FILE and returns nil if the file given to
|
||
it doesn't contain the org-mode #+TAGS: entry specified."
|
||
|
||
;; Step 1. Parse the eshell buffer for our tag between quotes
|
||
;; Make sure to move point to the end of the match:
|
||
(if (looking-at (rx "'" (group (one-or-more (not (or ")" "'"))))"'"))
|
||
(let* ((tag (match-string 1))
|
||
(reg (rx line-start
|
||
"#+" (optional "file") "tags:"
|
||
(one-or-more space)
|
||
(zero-or-more any)
|
||
(literal tag) word-end)))
|
||
(goto-char (match-end 0))
|
||
|
||
;; Step 2. Return the predicate function:
|
||
;; Careful when accessing the `reg' variable.
|
||
`(lambda (file)
|
||
(with-temp-buffer
|
||
(insert-file-contents file)
|
||
(re-search-forward ,reg nil t 1))))
|
||
(error "The `T' predicate takes an org-mode tag value in single quotes.")))
|
||
#+END_src
|
||
Then we need add that function to the =eshell-predicate-alist= as the =T= tag:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-add-predicates ()
|
||
"A hook to add a `eshell-org-file-tags' predicate filter to eshell."
|
||
(add-to-list 'eshell-predicate-alist '(?T . (eshell-org-file-tags))))
|
||
#+end_src
|
||
*Note:* We can’t add it to the list until after we start our first eshell session, so we just add it to the =eshell-pred-load-hook= which is sufficient.
|
||
* Eshell Functions
|
||
Any function that begins with =eshell/= can be called with the remaining letters. I used to have a function =eshell/f= as a replacement for =find=, but the [[https://github.com/sharkdp/fd][fd]] project is better. I used to have a number =g=-prefixed aliases to call git-related commands, but now, I just need to call [[file:ha-config.org::*Magit][Magit]] instead.
|
||
|
||
However, since =eshell= is an /Emacs/ shell, I want to think of how to use Emacs buffers in a shell-focused workflow. For instance, use =view-file= instead of =less=, as it will show a file with syntax coloring, and typing ~q~ returns to your shell session.
|
||
** Map
|
||
While I like eshell’s =for= loop well enough (if I can remember the syntax), as in:
|
||
#+begin_src sh :tangle no
|
||
for file in *.org {
|
||
chmod a+x $file
|
||
}
|
||
#+end_src
|
||
I like the idea of using a /map/ structure, as in:
|
||
#+begin_src sh :tangle no
|
||
map chmod a+x *.org
|
||
#+end_src
|
||
How would this work without special syntax? Well, eshell sends the =*.org= as a list of files, which we could use as the delimiter. The downside is that we want to list the files, we need to actually /list/ the files, as in:
|
||
#+begin_src sh :tangle no
|
||
map chmod a+x (list "a.org" "c.org")
|
||
#+end_src
|
||
Pretty ugly, but that isn’t my use-case. I could introduce syntax like:
|
||
#+begin_src sh :tangle no
|
||
map chmod a+x :: *.org b.txt
|
||
#+end_src
|
||
|
||
But what if the file isn’t the last element? Well, I could replace a /keyword/, =_=, with the filename when encountered.
|
||
|
||
Here is my initial function. After separating the arguments into two groups (split on the =::= string), we iterate over the file elements, creating a /form/ that includes the filename.
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/map (&rest args)
|
||
"Execute a command sequence over a collection of file elements.
|
||
The sequence and the elements are separated with a `::' string.
|
||
For instance:
|
||
|
||
map chmod a+x _ :: *.org b.txt
|
||
|
||
The `_' sequence is substituted with a single filename element,
|
||
and if not specified, the filename is appended to the command.
|
||
"
|
||
(seq-let (forms elements) (--split-when (equal it "::") args)
|
||
(dolist (element (-flatten (-concat elements)))
|
||
(let* ((form (if (-contains? forms "_")
|
||
(-replace "_" element forms)
|
||
(-snoc forms element)))
|
||
(cmd (car form))
|
||
(args (cdr form)))
|
||
(eshell-named-command cmd args)))))
|
||
#+end_src
|
||
The [[help:eshell-named-command][eshell-named-command]] takes the command separately from the arguments, so we use =car= and =cdr= on the form.
|
||
** Git
|
||
My =gst= command is just an alias to =magit-status=, but using the =alias= doesn't pull in the current working directory, so I make it a function, instead:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/gst (&rest args)
|
||
(magit-status (pop args) nil)
|
||
(eshell/echo)) ;; The echo command suppresses output
|
||
#+end_src
|
||
** Editing Files
|
||
Which an alias to [[help:find-file][find-file]] (which takes one argument), we could define a special function that can take multiple arguments, and open them in different windows. We first define a /helper function/ for dealing with multiple arguments. It takes two functions, the first function is called on the first argument, and the second function is called on each of the rest.
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-fn-on-files (fun1 fun2 args)
|
||
(unless (null args)
|
||
(let ((filenames (thread-last args
|
||
(reverse)
|
||
(-flatten)
|
||
(-map 'file-expand-wildcards)
|
||
(-flatten))))
|
||
(apply fun1 (car filenames))
|
||
(when (cdr filenames)
|
||
(-map fun2 (cdr filenames))))))
|
||
#+end_src
|
||
This allows us to replace some of our aliases with functions:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/e (&rest args)
|
||
"Edit one or more files in current window."
|
||
(eshell-fn-on-files 'find-file 'find-file-other-window args))
|
||
|
||
(defun eshell/ee (&rest args)
|
||
"Edit one or more files in another window."
|
||
(eshell-fn-on-files 'find-file-other-window 'find-file-other-window args))
|
||
#+end_src
|
||
We’ll leave the =e= alias to replace the =eshell= buffer window.
|
||
** Last Results
|
||
The [[https://github.com/mathiasdahl/shell-underscore][shell-underscore]] project looks pretty cool, where the =_= character represents a /filename/ with the contents of the previous command (you know, like if you were planning on it, you’d =tee= at the end of every command). An interesting idea that I could duplicate.
|
||
|
||
While diving into the =eshell= source code, I noticed that the special variable, =$$= can be used /sometimes/ as the output of the last command. For instance:
|
||
#+begin_example
|
||
$ echo "hello world"
|
||
hello world
|
||
$ echo $$
|
||
hello world
|
||
#+end_example
|
||
However, what I would like is something like this to work:
|
||
#+begin_example
|
||
$ ls *.org(U)
|
||
a.org b.org f.org
|
||
$ rg "foobar" $$
|
||
#+end_example
|
||
|
||
The problem /may/ be between calling Emacs functions versus external commands, as the =echo= works, but the call to =ls= doesn’t:
|
||
#+begin_example
|
||
$ ls *.org(U) b.txt
|
||
a.org b.org f.org b.txt
|
||
|
||
$ echo Nam $$
|
||
("Nam" nil)
|
||
#+end_example
|
||
|
||
However, we could add a hook that runs /after/ every command to copy the output to a variables of our choosing:
|
||
#+begin_src emacs-lisp
|
||
(defvar OUTPUT ""
|
||
"Contains the output from the last eshell command executed.")
|
||
|
||
(defvar LAST nil
|
||
"Contains a list of elements from the last eshell command executed.")
|
||
|
||
(defvar OUTAF ""
|
||
"Contains a filename that contains the output from the last eshell command.")
|
||
#+end_src
|
||
|
||
Why three variables? Well unlike the behavior of the original shell (and most of its descendents, like =bash=), =eshell= doesn’t automatically split on whitespace. For instance, =echo= called this way:
|
||
#+begin_example
|
||
$ echo a b *.txt
|
||
("a" "b"
|
||
("b.txt" "date today.txt"))
|
||
#+end_example
|
||
Is given a list of /three elements/: =a=, =b=, and a list of all files in the current directory with an =.org= extension. An interesting side-effect is that spaces in filenames are /often okay/. So we want =$OUTPUT= to contain the command’s output /as a string/, and we have, =$LAST= contains the same stuff, but separated by spaces, into a list. So, if we are passing the output from =ls= to =grep=, we would use =$LAST= to represent files. And, like the =shell-underscore= project mentioned earlier, I may want to have the output stored in a file, so =$OUTAF= will hold this temporary filename… you know, /OUTput As a File/, right?
|
||
|
||
The following function does the work of saving the output of the last command. We can get this because after every command, eshell updates two variables, [[elisp:(describe-variable 'eshell-last-input-end)][eshell-last-input-end]] (the start of the output), and [[elisp:(describe-variable 'eshell-last-output-start)][eshell-last-output-start]] (the end of the output):
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-store-last-output ()
|
||
"Store the output from the last eshell command.
|
||
Called after every command by connecting to the `eshell-post-command-hook'."
|
||
(setq OUTPUT
|
||
(s-trim
|
||
(buffer-substring-no-properties eshell-last-input-end eshell-last-output-start)))
|
||
(setq OUTAF (make-temp-file "ha-eshell-"))
|
||
(setq LAST (split-string OUTPUT))
|
||
|
||
;; Put the three values in the historical rings (see below):
|
||
(ha-eshell-store-output-history OUTPUT LAST OUTAF)
|
||
(ring-insert (gethash :text ha-eshell-output) OUTPUT)
|
||
(ring-insert (gethash :list ha-eshell-output) LAST)
|
||
(ring-insert (gethash :file ha-eshell-output) OUTAF)
|
||
|
||
(with-temp-file OUTAF
|
||
(insert OUTPUT)))
|
||
#+end_src
|
||
|
||
Now we save this output after every command by adding it to the [[elisp:(describe-variable 'eshell-post-command-hook)][eshell-post-command-hook]]:
|
||
#+begin_src emacs-lisp
|
||
(add-hook 'eshell-post-command-hook 'ha-eshell-store-last-output)
|
||
#+end_src
|
||
|
||
Success:
|
||
#+begin_example
|
||
$ ls *.org(U) b.txt
|
||
a.org b.txt
|
||
|
||
$ rg Nam $LAST
|
||
a.org
|
||
8:Nam vestibulum accumsan nisl.
|
||
|
||
b.txt
|
||
1:Nam euismod tellus id erat.
|
||
#+end_example
|
||
*** Accessing Output from the Past
|
||
It would also be great if you could grab /historical/ versions of those output. Instead of storing two or three objects to hold them, what about a hash table as a single interface?
|
||
#+begin_src emacs-lisp
|
||
(defvar ha-eshell-output (make-hash-table :size 3)
|
||
"A collection of rings representing the various historical output")
|
||
#+end_src
|
||
|
||
How would we store the historical lists? This is what [[info:elisp#Rings][rings]] are for:
|
||
#+begin_src emacs-lisp
|
||
(puthash :text (make-ring 10) ha-eshell-output)
|
||
(puthash :file (make-ring 10) ha-eshell-output)
|
||
(puthash :list (make-ring 10) ha-eshell-output)
|
||
#+end_src
|
||
|
||
The [[help:ha-eshell-store-last-output][ha-eshell-store-last-output]] function calls this function in order to store the results in the three rings:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-store-output-history (last-output last-list last-output-file)
|
||
"Store the LAST-OUTPUT as a string in a ring in the `ha-eshell-output'.
|
||
The LAST-LIST and LAST-OUTPUT-FILE are also store in separate rings."
|
||
(ring-insert (gethash :text ha-eshell-output) last-output)
|
||
(ring-insert (gethash :list ha-eshell-output) last-list)
|
||
(ring-insert (gethash :file ha-eshell-output) last-output-file))
|
||
#+end_src
|
||
|
||
How best to access this historical data. If there we some other shell, I might have variables like =$OUTPUT_3= or something. I think a function may be sufficient in practice. I’ll just call it [[help:eshell/output][output]] until something better comes along.
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/output (frmt &optional element)
|
||
"Return an eshell command output from its history.
|
||
The FORMAT represents how the output should be returned, and must
|
||
be `:text', `:list' or `:file'. The ELEMENT is the index into the
|
||
historical past, where `0' is the most recent, `1' is the next oldest, etc."
|
||
(if-let ((ring (gethash frmt ha-eshell-output)))
|
||
(ring-ref ring (or element 0))
|
||
""))
|
||
#+end_src
|
||
How would this function work in practice?
|
||
#+begin_example
|
||
$ echo (output :text 0) # The same as echo $OUTPUT
|
||
#+end_example
|
||
|
||
A bit verbose. I think some syntactic sugar functions would be in order:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/output-t (&optional element)
|
||
(eshell/output :text element))
|
||
|
||
(defun eshell/output-l (&optional element)
|
||
(eshell/output :list element))
|
||
|
||
(defun eshell/output-f (&optional element)
|
||
(eshell/output :file element))
|
||
#+end_src
|
||
|
||
How would this look? Something like:
|
||
#+begin_example
|
||
$ cat (output-f 4)
|
||
#+end_example
|
||
|
||
The final trick is being able to count backwards and remember they are always shifting. I guess if I wanted to remember the output for more than one command, I could do:
|
||
#+begin_example
|
||
$ setq OUTPUT_A $OUTPUT
|
||
#+end_example
|
||
* Special Prompt
|
||
Following [[http://blog.liangzan.net/blog/2012/12/12/customizing-your-emacs-eshell-prompt/][these instructions]], we build a better prompt with the Git branch in it (Of course, it matches my Bash prompt). First, we need a function that returns a string with the Git branch in it, e.g. ":master"
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun curr-dir-git-branch-string (pwd)
|
||
"Returns current git branch as a string, or the empty string if
|
||
PWD is not in a git repo (or the git command is not found)."
|
||
(interactive)
|
||
(when (and (not (file-remote-p pwd))
|
||
(eshell-search-path "git")
|
||
(locate-dominating-file pwd ".git"))
|
||
(let* ((git-url (shell-command-to-string "git config --get remote.origin.url"))
|
||
(git-repo (file-name-base (s-trim git-url)))
|
||
(git-output (shell-command-to-string (concat "git rev-parse --abbrev-ref HEAD")))
|
||
(git-branch (s-trim git-output))
|
||
(git-icon "\xe0a0")
|
||
(git-icon2 (propertize "\xf020" 'face `(:family "octicons"))))
|
||
(concat git-repo " " git-icon2 " " git-branch))))
|
||
#+end_src
|
||
|
||
The function takes the current directory passed in via =pwd= and replaces the =$HOME= part with a tilde. I'm sure this function already exists in the eshell source, but I didn't find it...
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun pwd-replace-home (pwd)
|
||
"Replace home in PWD with tilde (~) character."
|
||
(interactive)
|
||
(let* ((home (expand-file-name (getenv "HOME")))
|
||
(home-len (length home)))
|
||
(if (and
|
||
(>= (length pwd) home-len)
|
||
(equal home (substring pwd 0 home-len)))
|
||
(concat "~" (substring pwd home-len))
|
||
pwd)))
|
||
#+end_src
|
||
|
||
Make the directory name be shorter...by replacing all directory names with just its first names. However, we leave the last two to be the full names. Why yes, I did steal this.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun pwd-shorten-dirs (pwd)
|
||
"Shorten all directory names in PWD except the last two."
|
||
(let ((p-lst (split-string pwd "/")))
|
||
(if (> (length p-lst) 2)
|
||
(concat
|
||
(mapconcat (lambda (elm) (if (zerop (length elm)) ""
|
||
(substring elm 0 1)))
|
||
(butlast p-lst 2)
|
||
"/")
|
||
"/"
|
||
(mapconcat (lambda (elm) elm)
|
||
(last p-lst 2)
|
||
"/"))
|
||
pwd))) ;; Otherwise, we just return the PWD
|
||
#+end_src
|
||
|
||
Break up the directory into a "parent" and a "base":
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun split-directory-prompt (directory)
|
||
(if (string-match-p ".*/.*" directory)
|
||
(list (file-name-directory directory) (file-name-base directory))
|
||
(list "" directory)))
|
||
#+END_SRC
|
||
|
||
Using virtual environments for certain languages is helpful to know, especially since I change them based on the directory.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun ruby-prompt ()
|
||
"Returns a string (may be empty) based on the current Ruby Virtual Environment."
|
||
(let* ((executable "~/.rvm/bin/rvm-prompt")
|
||
(command (concat executable "v g")))
|
||
(when (file-exists-p executable)
|
||
(let* ((results (shell-command-to-string executable))
|
||
(cleaned (string-trim results))
|
||
(gem (propertize "\xe92b" 'face `(:family "alltheicons"))))
|
||
(when (and cleaned (not (equal cleaned "")))
|
||
(s-replace "ruby-" gem cleaned))))))
|
||
|
||
(defun python-prompt ()
|
||
"Returns a string (may be empty) based on the current Python
|
||
Virtual Environment. Assuming the M-x command: `pyenv-mode-set'
|
||
has been called."
|
||
(when (fboundp #'pyenv-mode-version)
|
||
(let ((venv (pyenv-mode-version)))
|
||
(when venv
|
||
(concat
|
||
(propertize "\xe928" 'face `(:family "alltheicons"))
|
||
(pyenv-mode-version))))))
|
||
#+end_src
|
||
|
||
Now tie it all together with a prompt function can color each of the prompts components.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun eshell/eshell-local-prompt-function ()
|
||
"A prompt for eshell that works locally (in that is assumes
|
||
that it could run certain commands) in order to make a prettier,
|
||
more-helpful local prompt."
|
||
(interactive)
|
||
(let* ((pwd (eshell/pwd))
|
||
(directory (split-directory-prompt
|
||
(pwd-shorten-dirs
|
||
(pwd-replace-home pwd))))
|
||
(parent (car directory))
|
||
(name (cadr directory))
|
||
(branch (curr-dir-git-branch-string pwd))
|
||
(ruby (when (not (file-remote-p pwd)) (ruby-prompt)))
|
||
(python (when (not (file-remote-p pwd)) (python-prompt)))
|
||
|
||
(dark-env (eq 'dark (frame-parameter nil 'background-mode)))
|
||
(for-bars `(:weight bold))
|
||
(for-parent (if dark-env `(:foreground "dark orange") `(:foreground "blue")))
|
||
(for-dir (if dark-env `(:foreground "orange" :weight bold)
|
||
`(:foreground "blue" :weight bold)))
|
||
(for-git `(:foreground "green"))
|
||
(for-ruby `(:foreground "red"))
|
||
(for-python `(:foreground "#5555FF")))
|
||
|
||
(concat
|
||
(propertize "⟣─ " 'face for-bars)
|
||
(propertize parent 'face for-parent)
|
||
(propertize name 'face for-dir)
|
||
(when branch
|
||
(concat (propertize " ── " 'face for-bars)
|
||
(propertize branch 'face for-git)))
|
||
;; (when ruby
|
||
;; (concat (propertize " ── " 'face for-bars)
|
||
;; (propertize ruby 'face for-ruby)))
|
||
;; (when python
|
||
;; (concat (propertize " ── " 'face for-bars)
|
||
;; (propertize python 'face for-python)))
|
||
(propertize "\n" 'face for-bars)
|
||
(propertize (if (= (user-uid) 0) " #" " $") 'face `(:weight ultra-bold))
|
||
;; (propertize " └→" 'face (if (= (user-uid) 0) `(:weight ultra-bold :foreground "red") `(:weight ultra-bold)))
|
||
(propertize " " 'face `(:weight bold)))))
|
||
|
||
(setq-default eshell-prompt-function #'eshell/eshell-local-prompt-function)
|
||
#+end_src
|
||
Here is the result:
|
||
[[http://imgur.com/nkpwII0.png]]
|
||
** Fringe Status
|
||
Some prompts, shells and terminal programs that display the exit code as an icon in the fringe. So can the [[http://projects.ryuslash.org/eshell-fringe-status/][eshell-fringe-status]] project. Seems to me, that if would be useful to rejuggle those fringe markers so that the marker matched the command entered (instead of seeing a red mark, and needing to scroll back in order to wonder what command it was that made it). Still...
|
||
#+begin_src emacs-lisp
|
||
(use-package eshell-fringe-status
|
||
:hook (eshell-mode . eshell-fringe-status-mode))
|
||
#+end_src
|
||
** Opening Banner
|
||
Whenever I open a shell, I instinctively type =ls= … so why not do that automatically? Perhaps we hook into the [[elisp:(describe-variable 'eshell-banner-load-hook)][eshell-banner-load-hook]]:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun ha-eshell-banner (&rest ignored)
|
||
"My personal banner."
|
||
(insert "ls")
|
||
(eshell-send-input))
|
||
|
||
(add-hook 'eshell-banner-load-hook 'ha-eshell-banner)
|
||
|
||
(setq eshell-banner-message "")
|
||
#+end_src
|
||
The only thing I would like is to not have the =ls= shown at the top of the buffer, nor added to the /history/. I’ll work on that some day.
|
||
* Shell Windows
|
||
Now that I often need to quickly pop into remote systems to run a shell or commands, I create helper functions to create those buffer windows. Each begin with =eshell-=:
|
||
** Shell There
|
||
The basis for opening an shell depends on the /location/. After that, we make the window smaller, give the buffer a good name, as well as immediately display the files with =ls= (since I instinctively just /do that/ … every time).
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-there (parent)
|
||
"Open an eshell session in a PARENT directory
|
||
in a smaller window named after this directory."
|
||
(let* ((name (thread-first parent
|
||
(split-string "/" t)
|
||
(last)
|
||
(car)))
|
||
(eshell-buffer-name (format "*eshell: %s*" name))
|
||
(height (/ (window-total-height) 3))
|
||
(default-directory parent))
|
||
(split-window-vertically (- height))
|
||
(eshell)))
|
||
#+end_src
|
||
** Shell Here
|
||
This version of the =eshell= is based on the current buffer’s parent directory:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-here ()
|
||
"Opens up a new shell in the directory of the current buffer.
|
||
The eshell is renamed to match that directory to make multiple
|
||
eshell windows easier."
|
||
(interactive)
|
||
(eshell-there (if (buffer-file-name)
|
||
(file-name-directory (buffer-file-name))
|
||
default-directory)))
|
||
#+end_src
|
||
And let’s bind it:
|
||
#+begin_src emacs-lisp
|
||
(bind-key "C-!" 'eshell-here)
|
||
#+end_src
|
||
** Shell for a Project
|
||
I usually want the =eshell= to start in the project’s root, using [[help:projectile-project-root][projectile-project-root]]:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-project ()
|
||
"Open a new shell in the project root directory, in a smaller window."
|
||
(interactive)
|
||
(eshell-there (projectile-project-root)))
|
||
#+end_src
|
||
And we can attach this function to the =projectile= menu:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader "p s" '("shell" . eshell-project))
|
||
#+end_src
|
||
|
||
** Shell Over There
|
||
Would be nice to be able to run an eshell session and use Tramp to connect to the remote host in one fell swoop:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-remote (host)
|
||
"Creates an eshell session that uses Tramp to automatically
|
||
connect to a remote system, HOST. The hostname can be either the
|
||
IP address, or FQDN, and can specify the user account, as in
|
||
root@blah.com. HOST can also be a complete Tramp reference."
|
||
(interactive "sHost: ")
|
||
|
||
(let ((destination-path
|
||
(cond
|
||
((string-match-p "^/" host) host)
|
||
|
||
((string-match-p (ha-eshell-host-regexp 'full) host)
|
||
(string-match (ha-eshell-host-regexp 'full) host) ;; Why!?
|
||
(let* ((user1 (match-string 2 host))
|
||
(host1 (match-string 3 host))
|
||
(user2 (match-string 6 host))
|
||
(host2 (match-string 7 host)))
|
||
(if host1
|
||
(ha-eshell-host->tramp user1 host1)
|
||
(ha-eshell-host->tramp user2 host2))))
|
||
|
||
(t (format "/%s:" host)))))
|
||
(eshell-there destination-path)))
|
||
#+END_SRC
|
||
** Shell Here to There
|
||
Since I have Org files that contains tables of system to remotely connect to, I figured I should have a little function that can jump to a host found listed anywhere on the line.
|
||
|
||
The regular expression associated with IP addresses, hostnames, user accounts (of the form, =jenkins@my.build.server=, or even full Tramp references, is a bit...uhm, hairy. And since I want to reuse these, I will hide them in a function:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-host-regexp (regexp)
|
||
"Returns a particular regular expression based on symbol, REGEXP"
|
||
(let* ((user-regexp "\\(\\([[:alnum:]._-]+\\)@\\)?")
|
||
(tramp-regexp "\\b/ssh:[:graph:]+")
|
||
(ip-char "[[:digit:]]")
|
||
(ip-plus-period (concat ip-char "+" "\\."))
|
||
(ip-regexp (concat "\\(\\(" ip-plus-period "\\)\\{3\\}" ip-char "+\\)"))
|
||
(host-char "[[:alpha:][:digit:]-]")
|
||
(host-plus-period (concat host-char "+" "\\."))
|
||
(host-regexp (concat "\\(\\(" host-plus-period "\\)+" host-char "+\\)"))
|
||
(horrific-regexp (concat "\\b"
|
||
user-regexp ip-regexp
|
||
"\\|"
|
||
user-regexp host-regexp
|
||
"\\b")))
|
||
(cond
|
||
((eq regexp 'tramp) tramp-regexp)
|
||
((eq regexp 'host) host-regexp)
|
||
((eq regexp 'full) horrific-regexp))))
|
||
#+END_SRC
|
||
|
||
The function to scan a line for hostname patterns uses different function calls that what I could use for =eshell-there=, so let's =save-excursion= and hunt around:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-scan-for-hostnames ()
|
||
"Helper function to scan the current line for any hostnames, IP
|
||
or Tramp references. This returns a tuple of the username (if
|
||
found) and the hostname.
|
||
|
||
If a Tramp reference is found, the username part of the tuple
|
||
will be `nil'."
|
||
(save-excursion
|
||
(goto-char (line-beginning-position))
|
||
(if (search-forward-regexp (ha-eshell-host-regexp 'tramp) (line-end-position) t)
|
||
(cons nil (buffer-substring-no-properties (match-beginning 0) (match-end 0)))
|
||
|
||
;; Returns the text associated with match expression, NUM or `nil' if no match was found.
|
||
(cl-flet ((ha-eshell-get-expression (num) (if-let ((first (match-beginning num))
|
||
(end (match-end num)))
|
||
(buffer-substring-no-properties first end))))
|
||
|
||
(search-forward-regexp (ha-eshell-host-regexp 'full) (line-end-position))
|
||
|
||
;; Until this is completely robust, let's keep this debugging code here:
|
||
;; (message (mapconcat (lambda (tup) (if-let ((s (car tup))
|
||
;; (e (cadr tup)))
|
||
;; (buffer-substring-no-properties s e)
|
||
;; "null"))
|
||
;; (-partition 2 (match-data t)) " -- "))
|
||
|
||
(let ((user1 (ha-eshell-get-expression 2))
|
||
(host1 (ha-eshell-get-expression 3))
|
||
(user2 (ha-eshell-get-expression 6))
|
||
(host2 (ha-eshell-get-expression 7)))
|
||
(if host1
|
||
(cons user1 host1)
|
||
(cons user2 host2)))))))
|
||
#+end_src
|
||
|
||
Tramp reference can be long when attempting to connect as another user account using the pipe symbol.
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-host->tramp (username hostname &optional prefer-root)
|
||
"Returns a TRAMP reference based on a USERNAME and HOSTNAME
|
||
that refers to any host or IP address."
|
||
(cond ((string-match-p "^/" host)
|
||
host)
|
||
((or (and prefer-root (not username)) (equal username "root"))
|
||
(format "/ssh:%s|sudo:%s:" hostname hostname))
|
||
((or (null username) (equal username user-login-name))
|
||
(format "/ssh:%s:" hostname))
|
||
(t
|
||
(format "/ssh:%s|sudo:%s|sudo@%s:%s:" hostname hostname username hostname))))
|
||
#+end_src
|
||
|
||
Finally, a function to pull it all together.
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-here-on-line (p)
|
||
"Search the current line for an IP address or hostname, and call the `eshell-here' function.
|
||
|
||
Call with PREFIX to connect with the `root' useraccount, via
|
||
`sudo'."
|
||
(interactive "p")
|
||
(destructuring-bind (user host) (ha-eshell-scan-for-hostnames)
|
||
(let ((destination (ha-eshell-host->tramp user host (> p 1))))
|
||
(message "Connecting to: %s" destination)
|
||
(eshell-there destination))))
|
||
#+end_src
|
||
|
||
* Better Command Line History
|
||
On [[http://www.reddit.com/r/emacs/comments/1zkj2d/advanced_usage_of_eshell/][this discussion]] a little gem for using IDO to search back through the history, instead of =M-R= to prompt for the history.
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-insert-history ()
|
||
"Displays the eshell history to select and insert back into your eshell."
|
||
(interactive)
|
||
(insert (completing-read "Eshell history: "
|
||
(delete-dups
|
||
(ring-elements eshell-history-ring)))))
|
||
#+END_SRC
|
||
* Command on the File Buffer
|
||
Sometimes you just need to change something about the current file you are editing...like the permissions or even execute it. Hitting =Command-1= will prompt for a shell command string and then append the current file to it and execute it.
|
||
#+begin_src emacs-lisp
|
||
(defun execute-command-on-file-buffer (cmd)
|
||
"Executes a shell command, CMD, on the current buffer's file.
|
||
If the filename is not specified, then it is appended to the cmd, so
|
||
|
||
chmod a+x
|
||
|
||
Works as expected. The special variable `$$' is replaced with the
|
||
filename of the buffer. Note that this is command is passed to
|
||
`eshell-command', so eshell modifiers are available, for
|
||
instance:
|
||
|
||
mv $$ $$(:r).txt
|
||
|
||
Will rename the current file to now have a .txt extension.
|
||
See `eshell-display-modifier-help' for details on that."
|
||
(interactive "sCommand to execute: ")
|
||
(let* ((file-name (buffer-file-name))
|
||
(full-cmd (cond ((string-match (rx "$$") cmd)
|
||
(replace-regexp-in-string (rx "$$") file-name cmd))
|
||
((string-match (rx (literal file-name)) cmd)
|
||
cmd)
|
||
(t
|
||
(concat cmd " " file-name)))))
|
||
(message "Executing: %s" full-cmd)
|
||
(eshell-command full-cmd)))
|
||
#+end_src
|
||
* Configuration
|
||
Here is where we associate all the functions and their hooks with =eshell=, through the magic of =use-package=.
|
||
|
||
Scrolling through the output and searching for results that can be copied to the kill ring is a great feature of Eshell. However, instead of running =end-of-buffer= key-binding, the following setting means any other key will jump back to the prompt:
|
||
#+begin_src emacs-lisp
|
||
(use-package eshell
|
||
:straight (:type built-in)
|
||
:init
|
||
(setq eshell-scroll-to-bottom-on-input 'all
|
||
eshell-error-if-no-glob t
|
||
eshell-hist-ignoredups t
|
||
eshell-save-history-on-exit t
|
||
|
||
;; Since eshell starts fast, let's get rid of it quickly:
|
||
eshell-kill-on-exit t
|
||
eshell-destroy-buffer-when-process-dies t
|
||
|
||
;; Can you remember the parameter differences between the
|
||
;; executables `chmod' and `find' and their Emacs equivalent? Me
|
||
;; neither, so this makes it act a bit more shell-like:
|
||
eshell-prefer-lisp-functions nil)
|
||
|
||
:hook ((eshell-pred-load . ha-eshell-add-predicates)
|
||
(eshell-exit . delete-window))
|
||
|
||
:bind (:map eshell-mode-map
|
||
("M-R" . eshell-insert-history)
|
||
("C-d" . ha-eshell-quit-or-delete-char)))
|
||
#+end_src
|
||
Note that the default list to [[emacs-lisp:(describe-variable 'eshell-visual-commands)][eshell-visual-commands]] is mostly good enough.
|
||
|
||
Finally, add some leader commands to call my previously defined functions:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader
|
||
"a e" '(:ignore t :which-key "eshell")
|
||
"a e e" '("new eshell" . eshell-here)
|
||
"a e r" '("remote" . eshell-remote)
|
||
"a e p" '("project" . eshell-project)
|
||
"a e g" '("at point" . eshell-here-on-line)
|
||
"a e !" '("exec on file-buffer" . execute-command-on-file-buffer))
|
||
#+end_src
|
||
No, i’m not sure why =use-package= has an issue with both =:hook=, =:bind= and =:config= directives in sequence.
|
||
* Technical Artifacts :noexport:
|
||
Let's =provide= a name so we can =require= this file:
|
||
#+begin_src emacs-lisp :exports none
|
||
(provide 'ha-eshell)
|
||
;;; ha-eshell.el ends here
|
||
#+end_src
|
||
|
||
#+DESCRIPTION: Emacs configuration for the Emacs Shell.
|
||
|
||
#+PROPERTY: header-args:sh :tangle no
|
||
#+PROPERTY: header-args:emacs-lisp :tangle yes
|
||
#+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes
|
||
|
||
#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
|
||
#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil
|
||
#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
|