The ultimate Emacs hacking tutorial in Windows 10 WSL 2

emacs.png◎ emacs.png

Do you think Emacs's performance in windows is bad? Do you really want to use a native Speed Emacs in Windows? Do you hate the unnatural path transition between windows convention and Linux convention? Do you feel frustrated when you try to install and configure Emacs in WSL? Do you just want to taste the power of Emacs running in WSL 2?

This tutorial may help you :)

What is WSL?

Earlier on August 2, 2016, Microsoft released Windows Subsystem for Linux (WSL), enabling the native way to run Linux Tools in Windows 10 and Windows Server 2019.

In May 2019, WSL 2 was introduced, by importing the Real Linux Kernel through Hyper-V features (in a Virtual Machine Environment), providing the users with the full & immerse way to work with Linux under windows, with 20 times the read/write performances of WSL 1.

For Windows Emacs Users, here are some advantages/disadvantages for you to consider before switching the WSL 2:

Advantages

  1. The performance of Magit is way faster than the GNU compiled original windows Emacs-27 binaries.

  2. The Font Rendering is better.

  3. No flickering.

  4. interoperability between Windows and Linux.

  5. Native OneDrive support.

  6. Super fast boot up time for Emacs.

Disadvantages

  1. The network configuration is a pain, workaround is available.

  2. X11 may lost connection when network changes.

More Details

More details of the differences between WSL 1 and WSL 2, check https://docs.microsoft.com/en-us/windows/wsl/compare-versions.

Taste WSL 2

Install and enable WSL 2

Update Windows 10

The first step is to make sure you have updated to the latest Windows 10. For Windows 10 Versions 1903 & 1909 users, make sure the minor version number is 1049, according to Microsoft's Devblog.

Install WSL 2

1
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

Enable WSL 2

1
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

Check WSL version

1
wsl -l -v

There are many people who may be not able to run the above commands, just make sure you have updated to the latest windows. https://devblogs.microsoft.com/commandline/wsl-2-support-is-coming-to-windows-10-versions-1903-and-1909/

Set default WSL version to 2

1
wsl.exe --set-default-version 2

All later WSL Distros are installed will be WSL 2. No worries, you can switch between WSL 1 and WSL 2 with just one command without pain, just do it~

Install Ubuntu 18.04 in Microsoft Store

ubuntu18.04.png◎ ubuntu18.04.png

Optional: Convert WSL 1 to WSL 2

If you already installed some Distros before but they are in WSL 1, no worries, it is very easy to switch from WSL 1 to WSL 2:

1
wsl.exe --set-version Ubuntu-18.04 2

Choosing the terminal

The first step to talk to WSL 2 is via Terminal Applications. In macOS, we have iTerm2, how about Windows? You may wonder we could just use the default Ubuntu Terminal, but it lacks UTF8 and Unicode supports, and only a few configuration options. Apparently, it is not the best option. We just need a more powerful and modern terminal. Luckily, Microsoft brings us another great product - Windows Terminal, and it is ranked with 67.6k stars in GitHub up to the time this article composed.

Windows Terminal has the following benefits compared with other terminals:

  1. Windows Terminal is multi-tabs and multi-panels.

  2. UTF8 & Unicode support.

  3. Supports Command Prompt, PowerShell, and WSL.

  4. GPU accelerated Text Rendering Engine.

  5. Custom Themes, styles and configurations.

  6. Modern and user-friendly.

  7. Open Source and Official Support.

    Install windows Terminal in Microsoft Store

windows-terminal.png◎ windows-terminal.png

Windows Terminal

Windows Terminal's setting is implemented in a json file - settings.json, every time you modify and save the file, it will take effects immediately, nice!

Add "cursorShape" and "fontFace" to the "defaults" section, it will apply on all tabs
1
2
3
4
5
6
"defaults":
{
    // Put settings here that you want to apply to all profiles.
    "cursorShape": "filledBox" ,
    "fontFace": "JetBrains Mono"
}
Update the home directory
1
2
3
4
5
6
7
8
{
    "guid": "{c6eaf9f4-32a7-5fdc-b5cf-066e8a4b1e40}",
    "hidden": false,
    "name": "Ubuntu-18.04",
    "source": "Windows.Terminal.Wsl",
    "startingDirectory": "//wsl$/Ubuntu-18.04/home/damonchan"

}
Disable keyboard cursor blink

Disable globally in keyboard setting:

UnblinkCursor.png◎ UnblinkCursor.png

zsh

I like to use zsh which provides more enhancement and configuration options compared with Bash.

1
2
3
sudo apt install zsh
chsh -s $(which zsh)
sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

Fonts

Just create a soft link (or copy all fonts) from Windows to Ubuntu:

1
2
3
4
5
# share the font by creating a soft link
# ln -s /mnt/c/Windows/Fonts ~/.fonts
# Or copy the fonts to wsl2's local directory for best performance, and also faster boot up time.
cp -r /mnt/c/Windows/Fonts ~/.fonts
fc-cache -fv

OneDrive

Just link Windows OneDrive Root Directory to Ubuntu:

1
ln -s /mnt/c/Users/elecm/OneDrive ~/OneDrive

X11 Server (VcXsrv)

In this tutorial, we choose VcXsrv as X11 server.

Install VcXsrv

1
choco install vcxsrv

Configure VcXsrv

  1. Start XLunch (VcXsrv)

    • Multiple windows -> Display number: -1 -> Next

      vcxsrv-1.png◎ vcxsrv-1.png

    • Start no client

      vcxsrv-2.png◎ vcxsrv-2.png

    • Tick Clipboard, Primary Selection, Native opengl, Disable access control (If you use socat to do X11 forwarding, Access control can be enabled, check Other Methods below) -> Next

      vcxsrv-3.png◎ vcxsrv-3.png

    • Save configuration PS:

      • Press "Win + R" -> insert shell:startup -> Press Enter

      • Save to startup can start X11 server when system boots

    • Finish

  2. allow both Private and Public network in windows firewall setting Control Panel\System and Security\Windows Defender Firewall\Allowed apps

    vcxsrv-5.png◎ vcxsrv-5.png

Disable /etc/resolv.conf generation

We disable /etc/resolv.conf generation, so that we can use custom name servers which points to google. vi /etc/wsl.conf

1
2
[network]
generateResolvConf = false

Setup custom name servers and point to google

vi /etc/resolv.conf

1
2
3
4
5
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateResolvConf = false
nameserver 8.8.8.8
nameserver 8.8.4.4

Export DISPLAY and LIBGL_ALWAYS_INDIRECT settings to ~/.zshrc

vi ~/.zshrc

1
2
export DISPLAY=$(ip route | awk '{print $3; exit}'):0
export LIBGL_ALWAYS_INDIRECT=1

Other Methods

There is another way to enable the X11 forwarding through socat, check this github issue. It is more safe but has some performance lost.

HiDPI

  1. Right click XLaunch (VcXsrv) -> Compatibility -> Change high DPI settings -> Tick Override high DPI scaling Behavior, Application.

    vcxsrv-4.png◎ vcxsrv-4.png

  2. Add the following statement to .zshrc

1
export GDK_SCALE=2

Bigger Cursor

Supposed VcXsrv is installed in C:\Program Files\VcXsrv\,

  1. Install big-cursor

    1
    
    sudo apt install big-cursor
  2. Rename C:\Program Files\VcXsrv\fonts\misc\cursor.pcf.gz to C:\Program Files\VcXsrv\fonts\misc\cursor-small.pcf.gz

  3. Copy /usr/share/fonts/X11/misc/big-cursor.pcf.gz from WSL to as C:\Program Files\VcXsrv\fonts\misc\cursor.pcf.gz

Setup Emacs

Let's dive into the meat of Compiling and installing the latest Emacs 27.1!

install dependencies

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
sudo apt install -y autoconf automake autotools-dev bsd-mailx build-essential \
    diffstat gnutls-dev imagemagick libasound2-dev libc6-dev libdatrie-dev \
    libdbus-1-dev libgconf2-dev libgif-dev libgnutls28-dev libgpm-dev libgtk2.0-dev \
    libgtk-3-dev libice-dev libjpeg-dev liblockfile-dev liblqr-1-0 libm17n-dev \
    libmagickwand-dev libncurses5-dev libncurses-dev libotf-dev libpng-dev \
    librsvg2-dev libsm-dev libthai-dev libtiff5-dev libtiff-dev libtinfo-dev libtool \
    libx11-dev libxext-dev libxi-dev libxml2-dev libxmu-dev libxmuu-dev libxpm-dev \
    libxrandr-dev libxt-dev libxtst-dev libxv-dev quilt sharutils texinfo xaw3dg \
    xaw3dg-dev xorg-dev xutils-dev zlib1g-dev libjansson-dev libxaw7-dev \
    libselinux1-dev libmagick++-dev libacl1-dev gir1.2-javascriptcoregtk-4.0 \
    gir1.2-webkit2-4.0 libenchant1c2a libglvnd-core-dev libicu-le-hb-dev \
    libidn2-0-dev libjavascriptcoregtk-4.0-dev liboss4-salsa2 libsoup2.4-dev \
    libsystemd-dev libwebkit2gtk-4.0-dev libx11-xcb-dev libxcb-dri2-0-dev \
    libxcb-dri3-dev libxcb-glx0-dev libxcb-present-dev libxshmfence-dev \
    x11proto-composite-dev x11proto-core-dev x11proto-damage-dev \
    x11proto-fixes-dev

Download, compile and install

1
2
3
4
5
6
7
8
cd ~
wget https://ftp.gnu.org/pub/gnu/emacs/emacs-27.1.tar.gz
tar -xzvf emacs-27.1.tar.gz
cd emacs-27.1
./configure
make
sudo make install
rm ~/emacs-27.1.tar.gz

Install doom

1
2
git clone --depth 1 https://github.com/hlissner/doom-emacs ~/.emacs.d
~/.emacs.d/bin/doom install

Start Emacs

1
emacs

Please notice:

  1. The first time to start Emacs may need some times (Every time the first time to start Emacs after system boots, it also needs some time at times), font setting, x11 checking, sound checking etc, please wait a moment. Normally it will finish within one or two minute.

  2. After refreshing font setting through fc-cache -fv, Emacs will take some time to configure the font setting, but it will only conduct one time.

  3. The second time to start Emacs will resume to the normal startup time.

Fix WSL_INTEROP issue

When you start Emacs, it will create another interop file, it make us can not start windows programs in Emacs, we can make it to use the one same interop file with the terminal.

Add the following to .zshrc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# fix interop
fix_wsl2_interop() {
	for i in $(pstree -np -s $$ | grep -o -E '[0-9]+'); do
		if [[ -e "/run/WSL/${i}_interop" ]]; then
			export WSL_INTEROP=/run/WSL/${i}_interop
		fi
	done
}

~/.emacs.d/bin/doom env > /dev/null 2>&1

Setup Audio

Add the following to .zshrc

1
export PULSE_SERVER=tcp:$(ip route | awk '{print $3; exit}')

In windows side, setup PulseAudio server, Follow https://github.com/microsoft/WSL/issues/5816

Optional: Setup Python Development Environment

pyenv

1
2
3
4
5
6
7
sudo apt-get install git gcc make openssl libssl-dev libbz2-dev libreadline-dev libsqlite3-dev libffi-dev
git clone https://github.com/pyenv/pyenv.git ~/.pyenv
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.zshrc
pyenv install 3.7.9
pyenv global 3.7.9

mypyls

mspyls can be installed by typing M-x lsp-install-server RET mspyls.

Optional: Setup Node.js Development Environment

node

Install nvm

1
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash

Add to .zshrc

1
2
3
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
1
2
3
4
nvm install --lts
nvm use --lts
npm i -g javascript-typescript-langserver
.emacs.d/bin/doom sync

Optional: Setup Rust Development Environment

1
2
3
4
5
6
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup component add rust-src
sudo curl -L https://github.com/rust-analyzer/rust-analyzer/releases/latest/download/rust-analyzer-linux -o /usr/bin/rust-analyzer
sudo chmod +x /usr/bin/rust-analyzer
cargo install cargo-check
rustup component add clippy-preview

Optional: Install other useful packages

apt

With apt

1
sudo apt install calibre sqlite3 pandoc

pdf-tools

1
2
sudo apt install libpng-dev zlib1g-dev libpoppler-glib-dev libpoppler-private-dev
M-x pdf-tools-install

rga

1
2
3
4
5
6
sudo apt install build-essential pandoc poppler-utils ffmpeg
wget https://github.com/phiresky/ripgrep-all/releases/download/v0.9.6/ripgrep_all-v0.9.6-x86_64-unknown-linux-musl.tar.gz
tar -zxvf ripgrep_all-v0.9.6-x86_64-unknown-linux-musl.tar.gz
cd ripgrep_all-v0.9.6-x86_64-unknown-linux-musl
sudo cp rga /usr/bin
sudo cp rga-preproc /usr/bin

Rime

Install and setup Rime
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sudo apt install git build-essential cmake libboost-all-dev libgoogle-glog-dev libleveldb-dev libmarisa-dev libopencc-dev libyaml-cpp-dev libgtest-dev
cd /usr/src/gtest
sudo cmake CMakeLists.txt
sudo make
#copy or symlink libgtest.a and libgtest_main.a to your /usr/lib folder
sudo cp *.a /usr/lib
cd ~/.emacs.d/librime
make
sudo make install
sudo apt-get install ibus-rime

PS: It is better to not share Rime User folder between windows and WSL, it may cause troubles.

In Emacs

1
M-x rime-compile-module
plum
1
2
curl -fsSL https://git.io/rime-install | bash
# rime_dir="$HOME/.rime" bash rime-install

sdcv

1
sudo apt install stardict sdcv

telega

1
2
3
4
5
6
7
8
9
sudo apt install gperf
git clone https://github.com/tdlib/td.git
cd td
mkdir build && cd build && cmake ../
make
sudo make install
git clone https://github.com/zevlg/telega.el
cd telega.el
make && make install

An .zshrc example

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:/usr/local/bin:$PATH

# Path to your oh-my-zsh installation.
export ZSH="/home/damonchan/.oh-my-zsh"

# Set name of the theme to load --- if set to "random", it will
# load a random theme each time oh-my-zsh is loaded, in which case,
# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
ZSH_THEME="robbyrussell"

# Set list of themes to pick from when loading at random
# Setting this variable when ZSH_THEME=random will cause zsh to load
# a theme from this variable instead of looking in $ZSH/themes/
# If set to an empty array, this variable will have no effect.
# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )

# Uncomment the following line to use case-sensitive completion.
# CASE_SENSITIVE="true"

# Uncomment the following line to use hyphen-insensitive completion.
# Case-sensitive completion must be off. _ and - will be interchangeable.
# HYPHEN_INSENSITIVE="true"

# Uncomment the following line to disable bi-weekly auto-update checks.
# DISABLE_AUTO_UPDATE="true"

# Uncomment the following line to automatically update without prompting.
# DISABLE_UPDATE_PROMPT="true"

# Uncomment the following line to change how often to auto-update (in days).
# export UPDATE_ZSH_DAYS=13

# Uncomment the following line if pasting URLs and other text is messed up.
# DISABLE_MAGIC_FUNCTIONS="true"

# Uncomment the following line to disable colors in ls.
# DISABLE_LS_COLORS="true"

# Uncomment the following line to disable auto-setting terminal title.
# DISABLE_AUTO_TITLE="true"

# Uncomment the following line to enable command auto-correction.
# ENABLE_CORRECTION="true"

# Uncomment the following line to display red dots whilst waiting for completion.
# COMPLETION_WAITING_DOTS="true"

# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY="true"

# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# You can set one of the optional three formats:
# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# or set a custom format using the strftime function format specifications,
# see 'man strftime' for details.
# HIST_STAMPS="mm/dd/yyyy"

# Would you like to use another custom folder than $ZSH/custom?
# ZSH_CUSTOM=/path/to/new-custom-folder

# Which plugins would you like to load?
# Standard plugins can be found in $ZSH/plugins/
# Custom plugins may be added to $ZSH_CUSTOM/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(git)

source $ZSH/oh-my-zsh.sh

# User configuration

# export MANPATH="/usr/local/man:$MANPATH"

# You may need to manually set your language environment
# export LANG=en_US.UTF-8

# Preferred editor for local and remote sessions
# if [[ -n $SSH_CONNECTION ]]; then
#   export EDITOR='vim'
# else
#   export EDITOR='mvim'
# fi

# Compilation flags
# export ARCHFLAGS="-arch x86_64"

# Set personal aliases, overriding those provided by oh-my-zsh libs,
# plugins, and themes. Aliases can be placed here, though oh-my-zsh
# users are encouraged to define aliases within the ZSH_CUSTOM folder.
# For a full list of active aliases, run `alias`.
#
# Example aliases
# alias zshconfig="mate ~/.zshrc"
# alias ohmyzsh="mate ~/.oh-my-zsh"

#export DISPLAY=:0
export DISPLAY=$(ip route | awk '{print $3; exit}'):0
export LIBGL_ALWAYS_INDIRECT=1

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
  eval "$(pyenv init -)"
fi

alias em="emacsclient -nw"

#export NVM_DIR="$HOME/.nvm"
#[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
#[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
export GDK_SCALE=2

# fix interop
fix_wsl2_interop() {
	for i in $(pstree -np -s $$ | grep -o -E '[0-9]+'); do
		if [[ -e "/run/WSL/${i}_interop" ]]; then
			export WSL_INTEROP=/run/WSL/${i}_interop
		fi
	done
}

~/.emacs.d/bin/doom env > /dev/null 2>&1

Export WSL (Backup WSL)

wsl ships with a –export option for users to do export the WSL distro to a tar file which can be imported to other machines:

1
wsl --export Ubuntu-18.04 Ubuntu-18.04_20200905

An Emacs Shortcut

Create two file in desktop: Emacs.sh and Emacs.bat

Emacs.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
cd ~
export DISPLAY=$(ip route | awk '{print $3; exit}'):0
export LIBGL_ALWAYS_INDIRECT=1

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
    eval "$(pyenv init -)"
fi

alias em="emacsclient -nw"

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
# export GDK_SCALE=2
emacs

Emacs.bat

1
2
@echo off
wsl ./emacs.sh

Double click Emacs.bat to launch Emacs.

Some Emacs hacking ideas

It is very easy to use Emacs to interactive with Windows's programs, such as browsing the URL with Chrome, open the PDF file with Acrobat Reader DC, open the current file with default program, launch explorer.exe, etc. Here are some ideas:

Browser URL with default browser

1
2
3
4
(defun wsl-browse-url-xdg-open (url &optional ignored)
  (interactive (browse-url-interactive-arg "URL: "))
  (shell-command-to-string (concat "explorer.exe " url)))
(advice-add #'browse-url-xdg-open :override #'wsl-browse-url-xdg-open)

If you a calibredb user, you can add the following advice to open the PDF/EPUB with windows default programs

1
2
3
4
5
6
;; calibredb
(defun wslcalibredb-open-with-default-tool (filepath)
  (shell-command-to-string
   (concat "cd " (shell-quote-argument (file-name-directory (expand-file-name filepath))) " && "
           (concat "cmd.exe /C start '' \"${@//&/^&}\" " (shell-quote-argument (file-name-nondirectory filepath))))))
(advice-add #'calibredb-open-with-default-tool :override #'wslcalibredb-open-with-default-tool)

Open the file with windows default programs or reveal it in explorer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
;;;###autoload

(defmacro wsl--open-with (id &optional app dir)
  `(defun ,(intern (format "wsl/%s" id)) ()
     (interactive)
     (wsl-open-with ,app ,dir)))

(defun wsl-open-with (&optional app-name path)
  "Send PATH to APP-NAME on WSL."
  (interactive)
  (let* ((path (expand-file-name
                (replace-regexp-in-string
                 "'" "\\'"
                 (or path (if (derived-mode-p 'dired-mode)
                              (dired-get-file-for-visit)
                            (buffer-file-name)))
                 nil t)))
         (command (format "%s `wslpath -w %s`" (shell-quote-argument app-name) path)))
    (shell-command-to-string command)))

(wsl--open-with open-in-default-program "explorer.exe" buffer-file-name)
(wsl--open-with reveal-in-explorer "explorer.exe" default-directory)
1
2
M-x wsl/open-in-default-program
M-x wsl/reveal-in-explorer

Footnotes

COMMENT Local Variables   ARCHIVE

updatedupdated2021-02-162021-02-16