Using Emacs on windows is always an imperfect experience, luckly windows10 comes with WSL2 now, which lets you install Emacs in WSL2 and use it on windows now. I did some hacking to make the experience little bit better, which includes:
- open any file on windows in emacs@wsl2
- setup org-capture on chrome on windows and capture notes using emacs@wsl2
- open links in org-mode in emacs@wsl2 using windows default apps. e.g., open https links using chrome on windows, open document links in MS-office on windows, etc.
- copy paste sharing between windows and wsl2
- fix for some keyboard shortcuts that’s been screened by windows by default
- setup outlook to create outlook links(uuid based) for emails/meetings/contacts in outlook, then open links in org-mode using outlook
- insert files into emacs@wsl2
lets go into the steps for each of the above fixes
precondition
On windows 10 you can install GWSL to enable emacs@wsl2 with gui, or you can setup your own VcXsrv so you can open emacs@wsl2 with gui.
open files on windows using emacs@wsl2
in order to add a context menu to open windows files in WSL2, i created a python file with content below:
import sys
import subprocess
import os
import re
import win32gui
class WindowMgr:
"""Encapsulates some calls to the winapi for window management"""
def __init__(self):
"""Constructor"""
self._handle = None
def find_window(self, class_name, window_name=None):
"""find a window by its class_name"""
self._handle = win32gui.FindWindow(class_name, window_name)
def _window_enum_callback(self, hwnd, wildcard):
"""Pass to win32gui.EnumWindows() to check all the opened windows"""
if re.match(wildcard, str(win32gui.GetWindowText(hwnd))) is not None:
self._handle = hwnd
def find_window_wildcard(self, wildcard):
"""find a window whose title matches the wildcard regex"""
self._handle = None
win32gui.EnumWindows(self._window_enum_callback, wildcard)
def set_foreground(self):
"""put the window in the foreground"""
win32gui.SetForegroundWindow(self._handle)
if __name__ == "__main__":
file_name = (
'"' + sys.argv[1] + '"'
if sys.argv[1].startswith("org-protocol")
else os.path.abspath(sys.argv[1]).replace("\\", "/").replace("C:", "/mnt/c")
)
wsl_path_prefix = "//wsl$/Ubuntu/"
if file_name.startswith(wsl_path_prefix):
file_name = file_name.replace(wsl_path_prefix, "/")
command = " ".join(
[
"C:/Windows/System32/wsl.exe",
"-d",
"Ubuntu",
"emacsclient",
"-n",
file_name,
]
)
subprocess.Popen(command, shell=True)
w = WindowMgr()
w.find_window_wildcard(".*Doom Emac")
w.set_foreground()
then i setup the context menu by the following reg file:
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\*\shell\Open in WSL Emacs] @="WSL Emacs" "icon"="\"C:\\Users\\username\\scoop\\apps\\emacs\\current\\share\\icons\\hicolor\\scalable\\apps\\emacs.ico\"" [HKEY_CLASSES_ROOT\*\shell\Open in WSL Emacs\Command] @="\"C:\\Users\\username\\scoop\\apps\\python\\current\\pythonw.exe\" \"C:\\Path\\to\\wsl_emacs.py\" \"%1\""
This works fine on windows as long you have pythonw.exe
and you have the icon file specified above exists
if you use python.exe
instead of pythonw.exe
, it still works but will always generate a cmd window when you call emacs@wsl2.
setup org capture for emacs@wsl2
if you already have the above script wsl_emacs.py
in place, then it is relatively simple, just run the reg file below
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\org-protocol]
@="URL:Org Protocol"
"URL Protocol"=""
[HKEY_CLASSES_ROOT\org-protocol\shell]
[HKEY_CLASSES_ROOT\org-protocol\shell\open]
[HKEY_CLASSES_ROOT\org-protocol\shell\open\command]
@="\"C:\\Users\\username\\scoop\\apps\\python\\current\\pythonw.exe\" \"C:\\Path\\to\\wsl_emacs.py\" \"%1\""
of coures you need to install the org-capture extension, and add your own capture template for org-capture, see more details at here
open links in org-mode in emacs@wsl2 using windows apps
this is done by installing the wslu on your wsl2, and then you can config your org-mode by:
;; "Set org-file-apps based on the system type"
(when IS-WSL
(setq org-file-apps '((remote . emacs)
(auto-mode . emacs)
(directory . emacs)
("\\.mm\\'" . "wslview \"%s\"")
("\\.x?html?\\'" . "wslview \"%s\"")
("\\.pptx?\\'" . "wslview \"%s\"")
("\\.xlsx?\\'" . "wslview \"%s\"")
("\\.docx?\\'" . "wslview \"%s\"")
("\\.pdf\\'" . "wslview \"%s\"")))
;; need to set explorer for open weblinks and htmls seperately
(setq browse-url-generic-program "/mnt/c/Program Files \(x86\)/Google/Chrome/Application/chrome.exe")
)
copy paste sharing between wsl2 and windows host
This can be easily done using GWSL above
fix keyboard shortcut for C-shift-0
I don’t know why but it seems windows blocks this keybind to be used. this is annoying, what i did is i remap this C-shift-0 to something i dont use with powertoys on windows, e.g., i remap C-shift-0 to C-shift-., then i have the following line to set my emacs keybinding
;; Windows 10 blocks Ctrl-Shift-0, So we using powertoy to cheat the system
;; when we press "C-)", it becomes "C->"
(cond (IS-WSL (map! "C->" #'sp-forward-slurp-sexp))
(t (map! "C-)" #'sp-forward-slurp-sexp)))
Outlook for Org-mode
add macro to outlook for copying link to objects in outlook
check developer in
File > Options > Customize Ribbon > Main Tabs > Developer
in developer tab create new VBA macro, and input:
Sub AddLinkToMessageInClipboard() Dim objMail As Object 'was earlier Outlook.MailItem Dim doClipboard As New DataObject Dim message As String 'One and ONLY one message muse be selected If Application.ActiveExplorer.Selection.Count <> 1 Then MsgBox ("Select one and ONLY one message.") Exit Sub End If Set objMail = Application.ActiveExplorer.Selection.Item(1) If objMail.Class = olMail Then doClipboard.SetText "[[outlook:" + objMail.EntryID + "][MESSAGE: " + objMail.Subject + " (" + objMail.SenderName + ")]]" ElseIf objMail.Class = olAppointment Then doClipboard.SetText "[[outlook:" + objMail.EntryID + "][MEETING: " + objMail.Subject + " (" + objMail.Organizer + ")]]" ElseIf objMail.Class = olTask Then doClipboard.SetText "[[outlook:" + objMail.EntryID + "][TASK: " + objMail.Subject + " (" + objMail.Owner + ")]]" ElseIf objMail.Class = olContact Then doClipboard.SetText "[[outlook:" + objMail.EntryID + "][CONTACT: " + objMail.Subject + " (" + objMail.FullName + ")]]" ElseIf objMail.Class = olJournal Then doClipboard.SetText "[[outlook:" + objMail.EntryID + "][JOURNAL: " + objMail.Subject + " (" + objMail.Type + ")]]" ElseIf objMail.Class = olNote Then doClipboard.SetText "[[outlook:" + objMail.EntryID + "][NOTE: " + objMail.Subject + " (" + " " + ")]]" Else doClipboard.SetText "[[outlook:" + objMail.EntryID + "][ITEM: " + objMail.Subject + " (" + objMail.MessageClass + ")]]" End If doClipboard.PutInClipboard End Sub
in visual basic editor window select
Tools > References
, if the option is disabled (grayed) then try to close all office apps and restart it, then clickbrowse...
and addFM20.dll
then run
SELFCERT.exe
in office folder and create personal certificatein visual basic editor window select
Tools > Digital signature
, choose the created certificate.you should be able to run the macro now, you can further add the macro to the quick access toolbar through
File > Options > QuickAccessToolbar > choose commands from: Macro > your Macro
Create registry entry for handling links to outlook object:
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\outlook] @="URL:Outlook Folders" "URL protocol"="" [HKEY_CLASSES_ROOT\outlook\DefaultIcon] @="C:\\Program Files\\Microsoft Office\\root\\Office16\\OUTLOOK.EXE" [HKEY_CLASSES_ROOT\outlook\shell] [HKEY_CLASSES_ROOT\outlook\shell\open] [HKEY_CLASSES_ROOT\outlook\shell\open\command] @="\"C:\\Program Files\\Microsoft Office\\root\\Office16\\OUTLOOK.EXE\" /select \"%1\""
Setup org-mode in your own emacs configuration put:
(org-link-set-parameters "outlook" :follow (lambda (id) (cond (IS-WSL (shell-command (concat "wslview " "outlook:" id))) (IS-WINDOWS (w32-shell-execute "open" (concat "outlook:" id))) )))
insert windows files into emacs@wsl2
since emacs runs in wsl, we cannot drag and drop files between windows and emacs@wsl2. so i wrote a simple script to get the path of files in windows, and convert it to path in wsl2
import os
import sys
file_path = os.path.abspath(sys.argv[-1]).replace("\\", "/").replace("C:", "/mnt/c")
wsl_path_prefix = "//wsl$/Ubuntu/"
if file_path.startswith(wsl_path_prefix):
file_path = file_path.replace(wsl_path_prefix, "/")
command = "echo " + file_path.strip() + "| clip"
os.system(command)
then I create the context menu WSL Path
by the registry file
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\*\shell\Copy WSL path]
@="WSL Path"
[HKEY_CLASSES_ROOT\*\shell\Copy WSL path\Command]
@="\"C:\\Users\\username\\scoop\\apps\\python\\current\\pythonw.exe\" \"C:\\path\\to\\wsl_path.py\" \"%1\""
share clipboard between windows host and WSL2
this is done by using powershell to get clipboard image saved and read the image into xclip for clipboard access in WSL2
(defun win2wsl-clipped-image()
"use powershell to save the clipped image to wsl and load it to xclip"
(let* ((powershell "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe")
(file-name "//wsl$/Ubuntu/home/mpnv38/tmp/clip_win2wsl.png")
(file-name-wsl "~/tmp/clip_win2wsl.png")
)
(shell-command (concat powershell " -noprofile -command \"(Get-Clipboard -Format Image).Save(\\\"" file-name "\\\")\""))
(call-process-shell-command (concat "xclip -selection clipboard -t image/png -i " file-name-wsl))
)
)
(advice-add 'org-download-clipboard :before #'win2wsl-clipped-image)
This is it, let me know in comments if you have any question or suggestion!