Using Emacs in a Terminal
2024-08-23 #emacs
Updates:
- 2024-09-06
- Mention the conflict between Tmux’s and Emacs’s mouse support. Update auto reattach with ssh config.
- 2024-09-03
- Added solution to open link in local browser. Added screenshot of my Emacs in terminal at the end. Improved code/config snippets.
Introduction
At my workplace, most coding tasks must be performed on a remote workstation (or a browser-based IDE) because source code is not permitted to be stored locally, and there are fewer development tools available on laptops.
To use Emacs for coding, I need to access the remote workstation through SSH or a remote desktop connection. I have tried various solutions, such as Tramp, Chrome Remote Desktop, GTK Broadway, xpra, and wprs. However, these solutions are either buggy, heavy, or slow. Ultimately, I have settled on the terminal-based approach using SSH and Tmux.
Issues
At first, I encountered several issues since things are not working out-of-the-box in the terminal.
- Emacs is no longer colorful.
- Some keybindings do not work.
- Cannot copy text to native clipboard.
- Cannot see the version control diff highlighting.
- Cannot open a link to a local browser easily.
Over time, I discovered solutions and workarounds for these issues, and started to enjoy some extra benefits I was not expecting.
- Allow using the same Emacs across multiple computers.
- Allow eliminating Crostini on my chromebook.
- Reduce the need of synchronizing files and resolving conflicts.
- Remote file editing is faster than Tramp.
- More native terminal from Tmux multiplexing (comparing to
vterm
orshell
, etc). - No font size scaling issue.
Solutions
Here are my tweaks to make Emacs usable from a terminal.
Pick a Good Terminal Emulator
First, pick an terminal emulator that at least
- supports true color, and
- supports OSC 52 (for system clipboard integration).
Ideally, it should also have fewer keybindings (or allow customization) to avoid conflicts with Emacs.
I am using the default terminal on Chrome OS (based on hterm), which meets all three requirements.
I do not use Linux, macOS or Windows enough nowadays to give a recommendation. However, I did tried macOS since I have one, the default terminal does not support true color (what a shame…) and iTerm2 has too much conflicting keybindings (M-x
, M-o
, M-p
, M-i
, M-j
, etc are all used by the terminal itself). A workaround is to map the Option
key to Esc+
.
Update: I have learned terminal graphics protocol from this reddit comment supported by very few terminal emulators at the moment. 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:
# .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.
Alternatively, add this to the .ssh/config
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 |
---|---|
To get true color Emacs when using terminal, set the environment COLORTERM
to truecolor
.
# .bashrc
export COLORTERM=truecolor
To get true color support in Tmux, add these to the .tmux.conf
:
# .tmux.conf
# See https://github.com/tmux/tmux/wiki/FAQ#how-do-i-use-rgb-colour.
set -as terminal-overrides ",xterm-256color:RGB"
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.
Open Link in Local Browser
I have two workarounds to open a web link from remote Emacs to local browser.
- use mouse click if the terminal emulator can recognize links and open it locally.
- copy the link to the local clipboard and paste it on the browser.
For (2), I add an advice to browse-url
to place the URL into the kill ring when in terminal. The link will be copied to the native clipboard via OSC 52. I then paste it into a local browser. I added key and mouse binding for the function to make copying more seamless. See my Emacs configuration.
(defun my/browse-url-advice (orig-fun &rest args)
"In terminal, copy the url to the kill ring for `browse-url'."
(if (display-graphic-p)
(apply orig-fun args)
(let ((url (car args)))
(message "Copied link: %s" url)
(kill-new url))))
(advice-add 'browse-url :around #'my/browse-url-advice)
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-;
toM-;
forflyspell-correct-wrapper
(C-;
is unsupported).C-c C-,
toC-c ,
fororg-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))))
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!
Finally, attach a screenshot of the setup above.
- On the left, it is
M-x list-colors-display
. - On the right, it is the source of this blog post.
- At the bottom, it is
M-x shell
with the output of a true color test script.
Fun fact: a coworker thought I was using a GUI application when they saw my terminal Emacs.