I love VTerm ! This tool is simple, responsive, and does exactly what you expect from a terminal. If you call VTerm from a tramp buffer (let’s say a remote host), it will automagically start from where you were.
If you apply shell-side configuration on your shell, then VTerm will track your working directory, help you open files from the terminal, and more.
However shell-side configuration is not doing great with tramp, since remote shell does not have the shell-side configuration files.
IMHO uploading personal configuration on a server, be it a vim setup, a fancy shell, or a shell-side configuration VTerm, is very bad mannered and hazardous, keep them at home please. tramp is the perfect tool since no remote side configuration is needed.
I realy want VTerm to be a bit like Eshell in terms of configurability, being able to have my aliases transfered from hosts to hosts.
But i really want Eshell to be as stable as VTerm in terms of responsivness, and stability.
Concerning Eshell, I prefer not leaving bash since I won’t be able to share my scripts to my coworkers, and I won’t do the same job twice.
With Vterm I found a way to :
- shell-side configure a server
- add my personal configuration
- do not impact/leave annoyance on server (only .bash_history)
The workflow is to M-x me/vterm-load-config
on a vterm buffer.
it will find my local bash config file, convert it to a string, do the same for the official shell-side configuration,
then insert those inside the terminal with the use of vterm-insert
.
Simple but effective.
Of course I did more than that:
- changed
vterm--get-directory
behaviour to be more flexible on hacky connections - added a helper function to better resolve tramp paths from vterm
- thought my bash function to be usable from remote
- inject payload inside a
{}
to make a single entry in history
Two files is all you need to run this hack:
- the personal functions (could be optional)
- the vterm config
Hopes this helps you, and you like bad grammar.
~/.doom.d/vterm/config.el
;;; vterm/config.el -*- lexical-binding: t; -*-
(after! vterm
;; Original functions overwrites tramp path with a guessed path.
;; However it breaks if remote fqdn/hostname is not resolvale by local machine
;; could also break on port forwarding, multihops,
;; custom protocol such as: docker, vagrant, ...
;; *if* you try to shell-side configure them.
;; Easily testable with vagrant ssh port on localhost.
;; My workflow is to open a tramp dired on / of the remote to get a
;; "foothold" then open vterms from there.
(defun vterm--get-directory (path)
"[OVERLOADED] Get normalized directory to PATH."
(when path
(let (directory)
(if (string-match "^\\(.*?\\)@\\(.*?\\):\\(.*?\\)$" path)
(progn
(let ((user (match-string 1 path))
(host (match-string 2 path))
(dir (match-string 3 path)))
(if (and (string-equal user user-login-name)
(string-equal host (system-name)))
(progn
(when (file-directory-p dir)
(setq directory (file-name-as-directory dir))))
(setq directory
;; Bellow is what i altered
(file-name-as-directory (concat (file-remote-p default-directory) dir))))))
(when (file-directory-p path)
(setq directory (file-name-as-directory path))))
directory)))
;; Injects the payload to the vterm buffer.
(defun me/vterm-load-config ()
"Pass local configuration files to vterm.
Allows remote vterm to be shell-side configured,
without altering remote config.
Also adds my personal configuration that does not rely
too much on external packages.
Prints a reasuring message to proove good faith."
(interactive)
(let (;; Bellow messages to reassure other users that look at history
(reasuring-message (format "Configuring shell of user %s to be emacs comptible"
user-full-name))
(reasuring-notice "This action is shell local, it will not affect other shells")
;; Bellow lies my configuration
(basic-func-script (f-read-text (concat (getenv "HOME")
"/.doom.d/shells/sources/functions.sh")))
;; Bellow lies the vterm shell-side configuration
;; Must be sourced last
(vterm-func-script (f-read-text (concat
(file-name-directory (find-library-name "vterm"))
"/etc/emacs-vterm-bash.sh"))))
(vterm-insert (format "# START: %s\n" reasuring-message))
(vterm-insert (format "# %s\n" reasuring-notice))
;; Create one single block in history
(vterm-insert "{\n")
(vterm-insert basic-func-script)
(vterm-insert vterm-func-script)
(vterm-insert "}\n")
;; End the single block in history
(vterm-insert (format "# %s\n" reasuring-notice))
(vterm-insert (format "# STOP: %s\n" reasuring-message))
)
)
;; find-file-other-window does not works great on remote:
;; if given an absolute path on a remote host,
;; the path will be understood as a local file since no
;; tramp prefix is present, and bash does not care
;; about tramp prefixes.
;; Bellow we solve context before sending it to
;; ffow
(defun me/vterm--find-file-other-window-wrapper (file)
"Help vterm find a FILE."
(find-file-other-window (me/vterm--ffow-resolver file)))
(defun me/vterm--ffow-resolver (file)
"Help vterm resolve FILE."
(cond
;; "/sudo::"
;; doom--sudo-file-path do the trick for us
((s-starts-with-p "/sudo::" file)
(doom--sudo-file-path
(concat (file-remote-p default-directory)
(substring-no-properties file 7))))
;; "/" means we want the "Relative root"
;; try appending the remote prefix if relevent
((s-starts-with-p "/" file)
(concat (file-remote-p default-directory) file))
;; we got a relative path
;; we don't need to help ffow to find it
(t
file)))
;; The variable vterm-eval-cmds is a SERIOUSLY SENSIBLE variable !
;; Do not be the guy that adds RCE into their config !
;; Allow customed ffow to be called from vterm
;; ffow should be as safe as find-file which is already trusted
;; we append our resolver that only manipulate strings,
;; Proove me wrong but i think it's safe.
(add-to-list 'vterm-eval-cmds '("find-file-other-window"
me/vterm--find-file-other-window-wrapper)))
~/.doom.d/shells/sources/functions.sh
#!/usr/bin/env bash
#
# Configures bash to interact with emacs
# supports:
# - vterm
# - M-x shell
# - term
# - shell from outside emacs
#
## Availables functions
# ALIAS DESCRIPTION FROM EXTERNAL TERMINAL FROM INSIDE EMACS
# e edit Send file to GUI, Send to server,
# returns jumps to it
# se sudo edit Send file to GUI, Send to server,
# returns jumps to it
# f file Open in terminal Send to server,
# jumps to it
# ff find-file Open in terminal Send to server,
# jumps to it
# sf sudo file Open in terminal Send to server,
# jumps to it
# sff sudo find-file Open in terminal Send to server,
# jumps to it
# to_buff send stdin to Open Buffer Send to server,
# temporary buffer in terminal jumps to it
#
### MISC:
# elisp run lisp script in running emacs server
# eshell Start Eshell in your terminal
# to_buff read pipeline and send it to a emacs buffer
# Emacs client
export ALTERNATE_EDITOR=""
export EMACS_SUDO_PREFIX="/sudo::"
_emacs_sudo () {
echo "$@" | xargs -n1 realpath | sed -e "s;^;$EMACS_SUDO_PREFIX;" | xargs >&2
echo "$@" | xargs -n1 realpath | sed -e "s;^;$EMACS_SUDO_PREFIX;" | xargs
}
_emacs_edit_from_inside () {
for file in "$@"
do
# VTERM support
if [[ -n ${EMACS_VTERM_PATH} ]]
then
vterm_cmd find-file-other-window "$file"
else
emacsclient -n -e "(find-file-other-window \"$file\")"
fi
done
}
_emacs_edit_from_outside () {
emacsclient -t "$@"
}
_emacs_sudo_edit_from_inside () {
_emacs_edit_from_inside "$(_emacs_sudo "$@")"
}
_emacs_sudo_edit_from_outside () {
_emacs_edit_from_outside "$(_emacs_sudo "$@")"
}
if [ -n "$INSIDE_EMACS" ]; then
# we are interacting from a shell/term inside of Emacs
# we do not want to use --tty since we will have nested emacs frames
export EDITOR="emacsclient" # $EDITOR opens in terminal
# export VISUAL="emacsclient -c -a emacs" # $VISUAL opens in GUI mode
alias e=_emacs_edit_from_inside
alias se=_emacs_sudo_edit_from_inside
alias f=_emacs_edit_from_inside
alias sf=_emacs_sudo_edit_from_inside
alias ff=_emacs_edit_from_inside
alias sff=_emacs_sudo_edit_from_inside
else
# we don't have to worry about nested frames
export EDITOR="emacsclient -t" # $EDITOR opens in terminal
# export VISUAL="emacsclient -c -a emacs" # $VISUAL opens in GUI mode
alias e=_emacs_edit_from_inside # open in emacs sever
alias se=_emacs_sudo_edit_from_inside
alias f=_emacs_edit_from_outside # open in terminal
alias sf=_emacs_sudo_edit_from_outside
alias ff=_emacs_edit_from_outside
alias sff=_emacs_sudo_edit_from_outside
fi
# Emacs lisp execute
elisp () {
emacsclient -n -t -e "$@"
}
# Emacs eshell
eshell () {
$EDITOR -e "(+eshell/here)"
}
# black="\[\e[0;30m\]"
# red="\[\e[0;31m\]"
green="\[\e[0;32m\]"
yellow="\[\e[0;33m\]"
blue="\[\e[0;34m\]"
# purple="\[\e[0;35m\]"
# cyan="\[\e[0;36m\]"
# white="\[\e[0;37m\]"
# orange="\[\e[0;91m\]"
normal="\[\e[0m\]"
# reset_color="\[\e[39m\]"
BASIC_PS1="${green}\u${normal}@${blue}\H${normal}:${yellow}\w${normal} ${green}\$${normal} "
# echo "----$PS1----"
PS1="$BASIC_PS1"
# Pipe STDIN to an emacs buffer
to_buff () {
# STDIN storage
tobufftmp="$(mktemp)"
trap 'rm -f -- "tobufftmp"' RETURN
# Reading STDIN
> "$tobufftmp" cat -
if [ -n "$INSIDE_EMACS" ]; then
# VTERM support
if [[ -n ${EMACS_VTERM_PATH} ]]
then
vterm_cmd find-file-other-window "$tobufftmp"
else
# xargs is there to strip the "" from the beginning
# and end of the output from Emacs.
emacsclient -n "$tobufftmp" | xargs
fi
else
emacsclient -t "$tobufftmp"
fi
}
if [[ "$INSIDE_EMACS" = 'vterm' ]] \
&& [[ -n ${EMACS_VTERM_PATH} ]] \
&& [[ -f ${EMACS_VTERM_PATH}/etc/emacs-vterm-bash.sh ]]; then
source "${EMACS_VTERM_PATH}/etc/emacs-vterm-bash.sh"
fi