The following section configures my Terminal experience, both inside and outside Emacs.
** Eshell
I used to use [[http://www.howardism.org/Technical/Emacs/eshell.html][Eshell all the time]], but now I've migrated most of /work/ directly into Emacs (rewriting all those shell scripts a Emacs Lisp code). However, a shell is pretty good for my brain at organizing files (old habits, maybe).
First, my /aliases/ follow me around, and the following creates the alias file, =~/.emacs.d/eshell/alias=:
Sure =iTerm= is nice for connecting and running commands on remote systems, however, it lacks a command line option that allows you to select and manipulate the displayed text without a mouse. This is where Emacs can shine.
*Feature One:*
When calling the =ha-ssh= function, it opens a =vterm= window which, unlike other terminal emulators in Emacs, merges both Emacs and Terminal behaviors. Essentially, it just works. It =vterm= isn't installed, it falls back to =term=.
Preload a list of favorite/special hostnames with multiple calls to:
Then calling =ha-ssh= function, a list of hostnames is available to quickly jump on a system (with the possibility of fuzzy matching if you have Helm or Ivy installed).
This also has the ability to call OpenStack to gather the hostnames of dynamic systems (what I call "an Overcloud"), which is appended to the list of favorite hostnames. The call to OpenStack only needs to be called once, since the hosts are then cached, see =ha-ssh-overcloud-query-for-hosts=.
*Feature Two:*
Use the /favorite host/ list to quickly edit a file on a remote system using Tramp, by calling either =ha-ssh-find-file= and =ha-ssh-find-root=.
*Feature Three:*
Working with remote shell connections programmatically, for instance:
#+BEGIN_SRC emacs-lisp :tangle no
(let ((win-name "some-host"))
(ha-ssh "some-host.in.some.place" win-name)
(ha-ssh-send "source ~/.bash_profile" win-name)
(ha-ssh-send "clear" win-name))
;; ...
(ha-ssh-exit win-name)
#+END_SRC
Actually the =win-name= in this case is optional, as it will use a good default.
I'm not giving up on Eshell, but I am playing around with [[https://github.com/akermu/emacs-libvterm][vterm]], and it is pretty good, but I use it primarily as a more reliable approach for remote terminal sessions.
VTerm has an issue (at least for me) with ~M-Backspace~ not deleting the previous word, and yeah, I want to make sure that both keystrokes do the same thing.
#+BEGIN_SRC emacs-lisp :tangle no
(use-package vterm
:init
(setq vterm-shell "/usr/local/bin/fish")
;; Granted, I seldom pop out to the shell except during code demonstrations,
;; but I like how C-p/C-n jumps up to each prompt entry using this setting
The advantage of running terminals in Emacs is the ability to copy text without a mouse. For that, hit ~C-c C-t~ to enter a special copy-mode. If I go into this mode, I might as well also go into normal mode to move the cursor.
*Note:* To exit the copy-mode (and copy the selected text to the clipboard), hit ~Return~.
Hrm. Seems that I might want a function to copy the output of the last command to a register, or even an org-capture...
;; I really like this =vterm= interface, so if I've got it loaded, let's use it:
(if (not (fboundp 'vterm))
;; Should we assume the =ssh= we want is on the PATH that started Emacs?
(make-term window-name "ssh" nil hostname)
(vterm ha-latest-ssh-window-name)
(vterm-send-string (format "ssh %s" hostname))
(vterm-send-return))
(pop-to-buffer ha-latest-ssh-window-name))
#+END_SRC
Of course, we need a function that =interactive= can call to get that list, and my thought is to call =helm= if it is available, otherwise, assume that ido/ivy will take over the =completing-read= function:
#+BEGIN_SRC emacs-lisp
(defun ha-ssh-choose-host ()
"Prompts the user for a host, and if it is in the cache, return
its IP address, otherwise, return the input given.
This is used in calls to =interactive= to select a host."
(let ((hostname
;; We call Helm directly if installed, only so that we can get better
;; labels in the window, otherwise, the =completing-read= call would be fine.
Simply calling =vterm= fails to load my full environment, so this allows me to start the terminal in a particular directory (defaulting to the root of the current project):
#+BEGIN_SRC emacs-lisp
(defun ha-shell (&optional directory)
"Creates and tidies up a =vterm= terminal shell in side window."
The previous functions (as well as my own end of sprint demonstrations) often need to issue some commands to a running terminal session, which is a simple wrapper around a /send text/ and /send return/ sequence:
#+BEGIN_SRC emacs-lisp
(defun ha-ssh-send (phrase &optional window-name)
"Send command PHRASE to the currently running SSH instance.
If you want to refer to another session, specify the correct WINDOW-NAME.
This is really useful for scripts and demonstrations."
(unless window-name
(setq window-name ha-latest-ssh-window-name))
(pop-to-buffer window-name)
(if (fboundp 'vterm)
(progn
(vterm-send-string phrase)
(vterm-send-return))
(progn
(term-send-raw-string phrase)
(term-send-input))))
#+END_SRC
On the rare occasion that I write a shell script, or at least, need to execute some one-line shell commands from some document, I have a function that combines a /read line from buffer/ and then send it to the currently running terminal:
#+BEGIN_SRC emacs-lisp
(defun ha-ssh-send-line ()
"Copy the contents of the current line in the current buffer,
and call =ha-ssh-send= with it. After sending the contents, it
returns to the current line."
(interactive)
;; The function =save-excursion= doesn't seem to work...
(let* ((buf (current-buffer))
(cmd-line (buffer-substring-no-properties
(line-beginning-position) (line-end-position)))
(trim-cmd (s-trim cmd-line)))
(ha-ssh-send trim-cmd)
;; (sit-for 0.25)
(pop-to-buffer buf)))
#+END_SRC
Let's have a quick way to bugger out of the terminal:
#+BEGIN_SRC emacs-lisp
(defun ha-ssh-exit (&optional window-name)
"End the SSH session specified by WINDOW-NAME (or if not, the latest session)."
Instead of making sure I have a list of remote systems already in the favorite hosts cache, I can pre-populate it with a call to OpenStack (my current VM system I'm using). These calls to the =openstack= CLI assume that the environment is already filled with the credentials. Hey, it is my local laptop ...
We'll give =openstack= CLI a =--format json= option to make it easier for parsing:
#+BEGIN_SRC emacs-lisp
(use-package json)
#+END_SRC
Need a variable to hold all our interesting hosts. Notice I use the word /overcloud/, but this is a name I've used for years to refer to /my virtual machines/ that I can get a listing of, and not get other VMs that I don't own.
#+BEGIN_SRC emacs-lisp
(defvar ha-ssh-overcloud-cache-data nil
"A vector of associated lists containing the servers in an Overcloud.")
#+END_SRC
If our cache data is empty, we could automatically retrieve this information, but only on the first time we attempt to connect. To do this, we'll =advice= the =ha-ssh-choose-host= function defined earlier:
#+BEGIN_SRC emacs-lisp
(defun ha-ssh-overcloud-query-for-hosts ()
"If the overcloud cache hasn't be populated, ask the user if we want to run the command."
(when (not ha-ssh-overcloud-cache-data)
(when (y-or-n-p "Cache of Overcloud hosts aren't populated. Retrieve hosts?")