Vterm shell-side configuration *EVERYWHERE* and more!

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
2 Likes