DEV Community

Cover image for Emacs keybindings to rule them all
Justin Tanner
Justin Tanner

Posted on • Edited on

Emacs keybindings to rule them all

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?

Are there any apps that already do this?

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:

  1. Selecting text with Ctrl+Space (start mark), move down a few lines to expand the text selection and Ctrl+Space (end mark)
  2. Prefix keys or chords shortcuts involving a modifier key and two other keystrokes such as Ctrl+xs (save)

Are there any scripts that do this?

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 A Lua based scripting language to intercept keystroke and mouse events (Mac)
  • AutoHotkey A scripting language that intercepts keystrokes and mouse events (Windows)

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.

Text selection in AutoHotkey

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
Enter fullscreen mode Exit fullscreen mode

Chords in Hammerspoon

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

Enter fullscreen mode Exit fullscreen mode

I need data structure

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.

Tables in Lua

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'
Enter fullscreen mode Exit fullscreen mode

Here is an equivalent Hash in Ruby:

user = {}
user[:name] = 'Yuval'
user[:job] = 'Author'
Enter fullscreen mode Exit fullscreen mode

Unfortunately Lua does not use the nice JSON style syntax for defining multiple properties such as:

user = { name: 'Yuval', job: 'Author' }
Enter fullscreen mode Exit fullscreen mode

In Lua that would look like:

user = { ['name'] = 'Yuval', ['job'] = 'Author' }

or

user = { name = 'Yuval', job = 'Author' }
Enter fullscreen mode Exit fullscreen mode

Lua also uses tables to create arrays:

fruits = { 'Apple', 'Banana', 'Orange' }

print(fruits[1])  -- Prints 'Apple' because Lua indexes arrays by 1 not 0.
Enter fullscreen mode Exit fullscreen mode

In Ruby that might be:

fruits = ['Apple', 'Banana', 'Orange']

puts fruits[0]
Enter fullscreen mode Exit fullscreen mode

Tables appeared to be the data structure I needed to store the keystrokes I wanted to translate.

Objects in AutoHotkey

In AuotHotKey associative arrays are called Objects.

User := {}
User["name"] := "Yuval"
User["job"] := "Author"
Enter fullscreen mode Exit fullscreen mode

AHK can use a JSON like syntax as well:

User := { name: 'Yuval', job: 'Author' }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The keystroke translation data structure

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'},
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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, ""]}}
Enter fullscreen mode Exit fullscreen mode

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.

Separating the text selection keys

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},
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Working around limitations with macros

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}
}
Enter fullscreen mode Exit fullscreen mode

This macro tries to emulate how Emacs deletes lines by first selecting the entire line, cutting it and then deleting it.

Compromising on chords

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.

Task switching

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 results

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.

Top comments (5)

Collapse
 
mengwong profile image
Meng Weng Wong

On a Mac, the way to get more emacs keybindings is through a file in ~/Library/KeyBindings/:

github.com/nileshk/mac-configurati...

Collapse
 
justintanner profile image
Justin Tanner

Fantastic, I didn’t know that conf file supported both “mark and point” and “chords”, it may be possible to ditch Hammerspoon on mac, i’ll have to give it a try.

Collapse
 
merrythemes profile image
Merry Themes

This is why I bought a macropad!

Collapse
 
dulm profile image
dulm

i'm painfully searching with google for solution for linux now, then click into this article, and find my spoon scrip ref ed by you supprisely :)
have you find any solution for linux ?

Collapse
 
justintanner profile image
Justin Tanner

Thanks for publishing your script it showed me that this was possible on Mac.

I haven't tried getting these scripts running on Linux yet (I'm not using Desktop Linux right now), but I wouldn't surprised if there was a window manager that was scriptable or configurable.

Let me know if you find something.