The Ultimate Emacs Hacking Tutorial in Windows 10 WSL 2

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 :)

PS: For better reading experience (Especially Inline-Codes), check my original blog:

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:


  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.


  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

Taste 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 and enable WSL 2

Install WSL 2

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

Enable WSL 2

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

Check WSL version

wsl -l -v

There are many people who may be not able to run the above commands, just make sure you have updated to lastest windows.

Set default WSL version to 2

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

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:

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’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
// Put settings here that you want to apply to all profiles.
"cursorShape": "filledBox" ,
"fontFace": "JetBrains Mono"
  • Update the home directory
"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:


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

sudo apt install zsh
chsh -s $(which zsh)
sh -c "$(curl -fsSL"


Just create a soft link from Windows to Ubuntu:

ln -s /mnt/c/Windows/Fonts ~/.fonts
fc-cache -fv


Just link Windows OneDrive Root Directory to Ubuntu:

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

X11 Server (VcXsrv)

In this tutorial, we choose VcXsrv as X11 server.

Install VcXsrv

choco install vcxsrv

Configure VcXsrv

  1. Start XLunch (VcXsrv)
  • Multiple windows -> Display number: -1 -> Next
  • Start no client
  • 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
  • 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

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

generateResolvConf = false

Setup custom name servers and point to google

vi /etc/resolv.conf

# 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

Export DISPLAY and LIBGL_ALWAYS_INDIRECT settings to ~/.zshrc

vi ~/.zshrc

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

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.


  1. Right click XLaunch (VcXsrv) -> Compatibility -> Change high DPI settings -> Tick Override high DPI scaling Behavior, Application.
  1. Add the following statement to .zshrc
export GDK_SCALE=2

Bigger Cursor

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

  1. Install big-cursor
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

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 \

Download, compile and install

cd ~
tar -xzvf emacs-27.1.tar.gz
cd emacs-27.1
sudo make install
rm ~/emacs-27.1.tar.gz

Install doom

git clone --depth 1 ~/.emacs.d
~/.emacs.d/bin/doom install

Start 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.


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

# 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
~/.emacs.d/bin/doom env > /dev/null 2>&1

Optional: Setup Python Development Environment


sudo apt-get install git gcc make openssl libssl-dev libbz2-dev libreadline-dev libsqlite3-dev libffi-dev
git clone ~/.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


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

Optional: Setup Node.js Development Environment


Install nvm

curl -o- | bash

Add to .zshrc

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

Optional: Setup Rust Development Environment

rustup component add rust-src
sudo curl -L -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


With apt

sudo apt install calibre sqlite3 pandoc


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


sudo apt install build-essential pandoc poppler-utils ffmpeg
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


Install and setup Rime

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

M-x rime-compile-module


sudo apt install stardict sdcv


sudo apt install gperf
git clone
cd td
mkdir build && cd build && cmake ../
sudo make install
git clone
cd telega.el
make && make install

An .zshrc example

# 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
# 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.
# Uncomment the following line to use hyphen-insensitive completion.
# Case-sensitive completion must be off. _ and - will be interchangeable.
# Uncomment the following line to disable bi-weekly auto-update checks.
# Uncomment the following line to automatically update without prompting.
# 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.
# Uncomment the following line to disable colors in ls.
# Uncomment the following line to disable auto-setting terminal title.
# Uncomment the following line to enable command auto-correction.
# Uncomment the following line to display red dots whilst waiting for completion.
# 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.
# 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"|""|"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.
source $ZSH/ 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 PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
eval "$(pyenv init -)"
alias em="emacsclient -nw"#export NVM_DIR="$HOME/.nvm"
#[ -s "$NVM_DIR/" ] && \. "$NVM_DIR/" # 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
~/.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:

wsl --export Ubuntu-18.04 Ubuntu-18.04_20200905

An Emacs Shortcut

Create two file in desktop: and Emacs.bat

cd ~
export DISPLAY=$(ip route | awk '{print $3; exit}'):0
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
eval "$(pyenv init -)"
alias em="emacsclient -nw"export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/" ] && \. "$NVM_DIR/" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
# export GDK_SCALE=2


@echo off
wsl ./

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

(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

;; calibredb
(defun wslcalibredb-open-with-default-tool (filepath)
(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

;;;###autoload(defmacro wsl--open-with (id &optional app dir)
`(defun ,(intern (format "wsl/%s" id)) ()
(wsl-open-with ,app ,dir)))
(defun wsl-open-with (&optional app-name path)
"Send PATH to APP-NAME on WSL."
(let* ((path (expand-file-name
"'" "\\'"
(or path (if (derived-mode-p 'dired-mode)
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)
M-x wsl/open-in-default-program
M-x wsl/reveal-in-explorer


Originally published at on Sep 6, 2020.



I love Programming, writing, and sharing interesting ideas. I believe only the ideas that we actually live are of any value.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store

I love Programming, writing, and sharing interesting ideas. I believe only the ideas that we actually live are of any value.