Wai Hon's Blog

Using Emacs in a Terminal

2024-08-23 #emacs

24-bit-color.png

Fun fact: a coworker thought I was using a GUI application when they saw my terminal Emacs.

Introduction

At my workplace, most coding tasks must be performed on a remote workstation (or a browser-based IDE) due to policies prohibiting local storage of source code and the limited development tools available on company laptops.

Coding with Emacs on a remote workstation requires access via SSH or a remote desktop connection. I have experienced with various solutions, including Tramp, Chrome Remote Desktop, GTK Broadway, xpra, and wprs. However, these proved buggy, resource-intensive, or slow. Consequently, I settled on the reliable terminal-based approach using SSH and Tmux.

Issues

Initially, I encountered several issues using Emacs in the terminal, as things were not working out-of-the-box:

  • Emacs was no longer colorful.
  • Some keybindings did not work.
  • I could not copy text to the native clipboard.
  • Version control diff highlighting was not visible.
  • Opening links in a local browser was not easy.

Over time, I discovered solutions and workarounds for these issues and began to enjoy some extra benefits I had not expected:

  • Using the same Emacs instance across multiple computers.
  • Eliminating Crostini on my Chromebook.
  • Reducing the need to synchronize files and resolve conflicts.
  • Remote file editing is faster than Tramp.
  • Providing a more native terminal experience via Tmux multiplexing (compared to vterm, shell, etc).
  • No font size scaling issue.

Solutions

Here's how I configured Emacs for comfortable terminal use.

Pick a Good Terminal Emulator

First, pick a terminal emulator that at least

  1. supports true color, and
  2. supports OSC 52 (for system clipboard integration).

Ideally, it should have fewer default keybindings or allow customization to avoid conflicts with Emacs.

The default Chrome OS terminal (hterm) is what I currently use, as it meets all three requirements. I do not use Linux, macOS, or Windows frequently enough nowadays to offer recommendations for those platforms.

Some terminals can even display images! E.g., the terminal graphics protocol by Kitty, the inline image protocol by iTerm2, and the Sixel protocol. It would be nice if there is Emacs integration with it!

Use Emacs in a Tmux Session

I use Emacs inside Tmux because it gives a persistent session that can be reattached from any computers or when losing SSH connection. I also reattach to the Tmux session on SSH connection automatically with this ssh config:

Host myhost
  RemoteCommand tmux -u new -A -D -s main
  RequestTTY yes

Alternative ssh command:

ssh -t myhost tmux -u new -A -D -s main

Alternative shell script approach:

# .bashrc
# Attach to the main Tmux session if it is
# - in SSH session
# - not inside Tmux
# - not inside Emacs
if [[ -n "$SSH_CLIENT" && -z "$TMUX" && -z "$INSIDE_EMACS" ]]; then
    # "-A" : reattach if session-name already exists
    # "-D" : detach other clients (ensure $SSH_TTY is always correct)
    tmux new -A -D -s main
fi

If I need an extra terminal, I create a new pane or window. I found the Tmux terminal better than vterm or shell because they are faster and are easily distinguishable from an Emacs buffer.

Optionally, change the default Tmux leader to reserve C-b for moving the cursor.

set-option -g prefix M-\\

Make Emacs Colorful

The screenshots below show the difference with and without true color (24-bit color) enabled.

Without true color, low contrast With true color, high contrast
emacs-without-truecolor.png emacs-with-truecolor.png

There are few ways to get true color Emacs in terminal,

  1. use a TERM that supports true color. e.g., xterm-direct. (won't work inside Tmux because it changes the term to tmux-256color)
  2. set the environment COLORTERM to truecolor (preferred)
# .bashrc
export COLORTERM=truecolor

There are few ways to get true color support in Tmux,

  1. use a TERM that support true color. e.g., xterm-direct, or
  2. start tmux with the RGB feature. e.g., tmux -T RGB, or
  3. override the features of your terminal in .tmux.conf. e.g.,
# .tmux.conf
# See https://github.com/tmux/tmux/wiki/FAQ#how-do-i-use-rgb-colour.
set -as terminal-overrides ",xterm-256color:RGB"

Tip: Use this script to test the true color.

Copy Text to Native Clipboard (OSC 52)

OSC 52 works by printing an unreadable sequence \033]52;c;[base64 data]\a so that the terminal will alter the system clipboard with the base64 data. To verify if the OSC 52 is working for your terminal setup, run printf "\033]52;c;$(printf "Hello, world" | base64)\a" from the terminal, it should put Hello, world to the system clipboard.

In the Emacs config, install the Clipetty package to sends text that you kill in Emacs to the native clipboard.

;; init.el
(use-package clipetty
  :hook (after-init . global-clipetty-mode))

To enable the clipboard support in Tmux, add these lines to the .tmux.conf:

# .tmux.conf
set -g set-clipboard on
# Required to make Clipetty works better on re-attach by appending
# "SSH_TTY" to "update-environment". See
# https://github.com/spudlyo/clipetty?tab=readme-ov-file#dealing-with-a-stale-ssh_tty-environment-variable
set -ag update-environment "SSH_TTY"

If it still does not work, try running printf verification command above and check the Tmux clipboard wiki.

Tweak for xterm-paste

When pasting from native clipboard, I would like to delete the region if there is an active one.

It is like the (delete-selection-mode) for xterm-paste.

(define-advice xterm-paste
    (:before (&args) delete-active-region)
  "Delete the selected text first before pasting from xterm."
  (when (use-region-p) (delete-active-region)))

Update Key Maps that Works in Terminal

The idea is to have a set of keybindings that terminal can response to.

This part varies from person to person. For example, I have remapped:

  • C-; to M-; for flyspell-correct-wrapper (C-; is unsupported).
  • C-c C-, to C-c , for org-insert-structure-template (C-, is unsupported).

Show Diff Highlighting with Margin

diff-hl does not work in the terminal by default, and this issue had annoyed me for a while until I searched for a solution. It turns out that diff-hl already has a solution by using "margin" to show the diff.

I added this elisp config to turn on diff-hl-margin-mode whenever I am inside a terminal.

;; init.el
(add-hook 'diff-hl-mode-on-hook
          (lambda ()
            (unless (display-graphic-p)
              (diff-hl-margin-local-mode))))
diff-hl-margin-mode.png

Enable Mouse Support

Don't forget to enable mouse support.

Note that there is a conflict where any mouse move over Emacs will deactivate the Tmux prefix key (fixed now in Tmux 3.5!). See https://github.com/tmux/tmux/issues/4111.

;; init.el
(xterm-mouse-mode +1)
# .tmux.conf
# Whether to enable mouse support in Tmux (switch windows and pane,
# resize pane, etc). Setting mouse on or off does not disable Emacs's
# xterm-mouse-mode.
set -g mouse on

Conclusion

For most Emacsers, GUI Emacs is still a better choice because it:

  • Has no network latency.
  • Has better multi-media supports (images, PDF, etc).
  • Can handle all keybindings.
  • Can interact with clipboard natively.
  • Can open link to local browser easily.
  • Can customize more (font size per buffer, tool-tip, etc).

However, if you, like me, need to do stuff remotely or want to have the same Emacs across multiple computers by SSH+Tmux, at the expense of losing the aforementioned GUI features, I hope the above tricks can make your setup better!