Apr 4, 2019 ⋅ Justin Tanner
When Microsoft released Ubuntu for Windows 10 I took the opportunity to try web development on Windows after 8 years on a Mac. I was pleasantly surprised with Ubuntu on Windows 10. Having Ubuntu in your terminal is handy because it more closely resembles the servers I deploy my code to. But there was one frustrating aspect of switching back to Windows, keyboard shortcuts.
Some differences are easy to workaround such as Ctrl+c
and Cmd+c
(copy). Copy and paste can be dealt with by changing settings in the OS, but other shortcuts are quite different such as Cmd+q
and Alt+F4
(quit an app). Instead of memorizing these differences I wondered if there was a better solution.
My text editor of choice is Emacs. Once inside Emacs you enter another world of keyboard shortcuts. Goodbye Ctrl+c
and hello Ctrl+y
. Navigating text starts by holding down a modifier key such as Ctrl
or Alt
and your hands never leave the home row. Would it be possible to use Emacs shortcuts to control both operating systems in the same way?
I started by looking for something I could download or buy to solve the problem. For Windows I found XKeymacs which described it self as “a keyboard utility to realize Emacs like-usability on all windows applications”.
On Mac I couldn’t find any app to solve the problem, but OSX supports most Emacs shortcuts out of the box. For example Ctrl+a
is mapped to the start of the line character just like Emacs.
Both XKeymacs and the default shortcuts for Mac failed to support two important Emacs features:
Ctrl+Space
(start mark), move down a few lines to expand the text selection and Ctrl+Space
(end mark)Ctrl+xs
(save)Next I searched Github for solutions and I found some interesting, scripts. These scripts didn’t solve the problems of selecting text or chord keys, but they were written in two apps / frameworks that were new to me.
Hammerspoon and AutoHotkey had potential, but I needed to make sure they could do what I wanted, so I wrote some experimental code in these new environments.
First I set out to see if AutoHotkey could emulate Emacs style text selection.
#SingleInstance
#Installkeybdhook
#UseHook
SetKeyDelay 0
global selecting := False
SendWithShift(keys)
{
If (selecting)
{
Send +%keys%
}
Else
{
Send %keys%
}
}
^Space::
selecting := !selecting
Return
^n::
SendWithShift("{Down}")
Return
^p::
SendWithShift("{Up}")
Return
^f::
SendWithShift("{Right}")
Return
^b::
SendWithShift("{Left}")
Return
^a::
SendWithShift("{Home}")
Return
^e::
SendWithShift("{End}")
Return
Next I checked if Hammerspoon could execute chord keys.
local ctrlXActive = false
local hotkeyModal = hs.hotkey.modal.new()
function startCtrlX()
ctrlXActive = true
hs.timer.doAfter(1, clearCtrlX)
end
function clearCtrlX()
print('Clearing ctrlXActive flag.')
ctrlXActive = false
end
function forwardOrOpen()
hotkeyModal:exit()
if ctrlXActive then
print('Opening file.')
hs.eventtap.keyStroke('cmd', 'o')
else
print('Moving cursor forward.')
hs.eventtap.keyStroke(nil, 'right')
end
hotkeyModal:enter()
clearCtrlX()
end
hotkeyModal:bind({'ctrl'}, 'x', startCtrlX, nil, nil)
hotkeyModal:bind({'ctrl'}, 'f', forwardOrOpen, nil, nil)
hotkeyModal:enter()
Success! The experiments showed me that Hammerspoon and AutoHotkey could emulate Emacs in the way I wanted. Next I needed a data structure to store all the information needed to translate keystrokes.
Hammerspoon is scripted in Lua a dynamic language similar to Ruby. Associative arrays in Ruby are called hashes and in Lua they are called tables.
user = {}
user['name'] = 'Yuval'
user['job'] = 'Author'
Here is an equivalent Hash in Ruby:
user = {}
user[:name] = 'Yuval'
user[:job] = 'Author'
Unfortunately Lua does not use the nice JSON style syntax for defining multiple properties such as:
user = { name: 'Yuval', job: 'Author' }
In Lua that would look like:
user = { ['name'] = 'Yuval', ['job'] = 'Author' }
or
user = { name = 'Yuval', job = 'Author' }
Lua also uses tables to create arrays:
fruits = { 'Apple', 'Banana', 'Orange' }
print(fruits[1]) -- Prints 'Apple' because Lua indexes arrays by 1 not 0.
In Ruby that might be:
fruits = ['Apple', 'Banana', 'Orange']
puts fruits[0]
Tables appeared to be the data structure I needed to store the keystrokes I wanted to translate.
In AuotHotKey associative arrays are called Objects.
User := {}
User["name"] := "Yuval"
User["job"] := "Author"
AHK can use a JSON like syntax as well:
User := { name: 'Yuval', job: 'Author' }
Similar to Lua AHK uses Object’s to define both associative arrays and regular arrays:
Fruits := ['Apple', 'Banana', 'Orange']
OutputDebug %Fruits[1]% ; Prints 'Apple' because AHK also uses 1-based array indexes
To store a look-up table for keystrokes I started with a Lua table like this:
local keys = {
['ctrl'] = {
...
['b'] = {nil, 'left'},
['w'] = {'cmd', 'x'},
...
}
}
Hitting Ctrl+b
would trigger the left directional keystroke which doesn’t require holding a modifier key such as Ctrl
and hitting Ctrl+w
would trigger the Mac cut shortcut Cmd+x
.
This config separates modifiers and regular keys into different array entries because that’s how Hammerspoon’s API activates keys.
hs.eventtap.event.newKeyEvent(mods, key, true):post()
hs.eventtap.event.newKeyEvent(mods, key, false):post()
In AutoHotkey modifiers and regular keystrokes can be intermixed. Here is the the same data structure in AHK:
global keys
:= : {"ctrl"
: {"b": ["{Left}"]
,"w": ["^x", False, ""]}}
Ctrl+w
triggers the Windows cut shortcut Ctrl+x
which is ^x
in AHK. This syntax obeys the multi-line continuation rules in AHK making sure to start every line with a character indicating that this data structure will span multiple lines.
After starting a text selection with Ctrl+Space
in Emacs you can navigate around the document to increase or decrease the size of the selection with the navigation shortcuts like Ctrl+n
(down). In contrast other shortcuts can’t work while text is selected such as Ctrl+k
(delete a line of text). To support text selection my config data needed to differentiate these two types of shortcuts.
local keys = {
['ctrl'] = {
...
['b'] = {nil, 'left', true},
['d'] = {'ctrl', 'd', false},
...
}
}
Ctrl+b
shrinks the current text selection by moving one character backwards so it’s marked with true
as the third config setting. Ctrl+d
deletes a character at the current cursor position which needs to first deselect the current text selection, so it’s marked with false
as the third config setting.
The final additional to the data structure was an option to execute a Macro instead of a keystroke. Here is a macro written in AHK:
,"k": ["", False, "MacroKillLine"]
...
; Macro to kill a line and add it to the clipboard
MacroKillLine()
{
Send {HOME}{ShiftDown}{END}{ShiftUp}
Send ^x
Send {Del}
}
This macro tries to emulate how Emacs deletes lines by first selecting the entire line, cutting it and then deleting it.
While I was working my way through the default Emacs keybindings I noticed some shortcuts in Emacs I’d never thought about.
File file is bound to: Ctrl-x Ctrl-f
(release Ctrl between typing x and f)
and
Set fill column is bound to: Ctrl-x f
(hold Ctrl between typing x and f)
These shortcuts preform different actions, but exploit the subtle difference of letting go of Ctrl vs holding down Ctrl. Should I support these shortcuts in my scripts? After reviewing the handful of Emacs shortcuts that differ in this way I decided to drop support for them in the scripts because I wasn’t using them anyway. I was probably typing these shortcuts by accident in Emacs. I decided to disable those shortcuts in Emacs with an elisp config script.
One important shortcut not related editing text is Alt+Tab
or Cmd+Tab
(the task switcher). With my scripts complete I noticed that Alt+Tab
was the only reason my hands were leaving the home-row. Could I rebind these shortcuts to some unassigned Emacs shortcuts and keep my hands on the home-row?
All of the shortcuts ranging from A to Z using Ctrl
were already taken. What about an unassigned chord keys like Ctrl-xt
?
I hooked up the Ctrl+xt
shortcut and immediately hit a problem. On Windows Alt+Tab
requires holding down the Alt
key and tapping the Tab
key to step through each currently running app, but I wanted to hit Ctrl-xt
and then Tab
to step through the currently running apps. The code that handled chords in my scripts would not support this kind of shortcut.
On stackoverflow I found other programmers struggling with the same problem in AHK. One solution on Windows was to use Alt+Esc
instead of Alt+Tab
. Alt+Esc
switches to the last app that was opened without a user interface. This was compatible with my script, but in practice I found it confusing to switch app with this shortcut. Trying to script Alt+Tab
and Cmd+Tab
seemed like a dead end.
As a last resort I looked for alternative task switchers to the ones built into each operating system. For Windows I found a free open source project switcheroo. I set Ctrl+xt
to launch switcheroo instead of Alt+tab
and everything was peachy. On Mac I took the same approach and purchased a paid app called Witch 3.
The scripts worked! I using them daily on both Windows and Mac. Switching between operating systems is no longer a pain.
If you wanna give them a try for yourself checkout the Github repository and happy scripting.