"Spacebar Heating" - bringing Dvorak to Obsidian

Kacper Darowski 3/22/2024 105
Thumbnail

As though being a university student isn't hard enough, I make my life extra difficult by transcribing the lecture slides live in Markdown with embedded TeX. (There is a reason this blog functions exactly on that principle, too!)

As you might imagine, keeping up with the professor's pace is quite the challenge. To accomplish that, I rock a rather peculiar workflow that has earned me the title of "the xkcd spacebar heating guy" in my friend group - and I carry it with honour. Let me tell you a story...

"Workflow" | [xkcd.com](https://xkcd.com/1172/)
"Workflow" | xkcd.com

The Backstory

I am a Vim user. I am also a Dvorak user.

I'll be the first to admit - that's a very rare combination. All the more surprising that Vim has a built-in way to accomodate this - my beloved "langmap".

Let's slow down first. What do all of those words even mean?

What is Vim?

Vim is an ancient command line based text editor that utilizes your whole keyboard as "shortcuts". But how do you actually type then? Majorly simplified, Vim starts in "Normal Mode", in which all your keys do something. For example, h moves the cursor one character to the left, j moves it one character up, k one character down, and l one character right. That's not the only mode though! Pressing i takes you to the "Insert Mode", which functions like any standard text editor - whatever you type is simply inserted in the file. You can exit that mode by pressing Esc or <C-c>, which is Vim-talk for Ctrl+c.

The first implementation of this idea (Vi, the predecessor of Vim) was released in 1976 - nearly 50 years ago - so why do people still use it? The learning curve is steep but very rewarding in the end. Vim significantly increases your text-editing speed. One could say, text-editors already peaked with Vi's release - not Emacs users though!

What is Dvorak?

I could never explain it better than The Dvorak Zine, but I'll try to give you a fair idea. The dominant QWERTY keyboard layout is de-facto the default, hence it might come as a surprise that very little thought was actually put into that layout. The major reason behind your keys not being alphabetically ordered in first place, is that clicking two adjacent keys on a typewriter in rapid succession would cause it to jam. As a result, typists had to be slowed down or at least the keys had to be spread across the board. QWERTY tried to solve that by trial and error. The keys are arranged, well, randomly. In fact, the top row of your keys spells "typewriter" if rearranged slightly! How crazy is that.

Dr. August Dvorak must've been baffled as well, as he studied letter distributions, common pairs of letters, typing practices, and more, to develop an alternate keyboard layout - the Dvorak layout. The typewriter jamming problem was quickly solved through technological improvements, so that Dr. Dvorak could focus on comfort and speed of typing instead. In short, the layout puts the most common keys in the "Home row" of the keyboard - right where your fingers rest. This ensures that you don't have to move your fingers at all for about 70% of your keystrokes! Furthermore, the consonants are below your left hand, while the vocals are below your right hand, so you naturally switch between the two hands all the time, enabling the currently inactive hand to prepare for the next stroke.

Personally, I switched to the Dvorak layout to improve my typing speed, but most importantly, reduce the strain on my wrists. Being at higher risk of carpal tunnel syndrome, I took the pain felt after long coding/writing sessions seriously. After switching to Dvorak, I never experience any typing-related strain anymore. The unfortunate issue is - the world is built around QWERTY, so I'm swimming against the flow here.

Distribution of the most common keys on QWERTY and Dvorak | [Patrick Wied](https://www.patrick-wied.at/projects/heatmap-keyboard/)
Distribution of the most common keys on QWERTY and Dvorak | Patrick Wied

What is a langmap?

As we've learned, Dvorak completely rearranges the keyboard. In combination with Vim, this leads to two issues:

  • Certain Vim keybindings are meant to be easily accessible - hjkl sits nicely beneath your right hand's resting position. Those keys land on completely different positions on Dvorak.
  • If I were to learn Vim with Dvorak's rearranged keys, my muscle memory would never let me use Vim on someone else's QWERTY setup.

This is simply mittigated by setting an appropriate langmap. At core, the language map is meant to enable people writing in a different script - like Greek or Cyryllic, to use their favourite text editor without any issues. Basically, you tell Vim to interpret "α" or "β" as "a" or "b" respectively etc., while in Normal Mode. This allows you to use Vim's bindings with a Greek keyboard layout, as though you were using standard QWERTY, however you still type using the Greek alphabet in Insert Mode!

That is perfect for us! We can set up Vim in such a way that our keyboard behaves like QWERTY in Normal Mode, but like Dvorak in Insert Mode. But I wouldn't be writing this post if everything truly were perfect, would I?

The Problem

The concept behind Vim is genius for coding or writing efficiently. As such, many programs let you emulate Vim. Text editors like VsCode, IDEs like IntelliJ Idea, note-taking apps like Joplin or Obsidian all implement some kind of Vim mappings. The issue for us: Langmaps are such a nieche feature that they're never considered.

I don't fully blame developers who choose to neglect langmaps. After all, the community behind Vim itself seems to have forgotten about it. Unfortunately though, it completely disables me and people alike from using such a program's Vim emulation.

At last, we come full circle back to my quest of speedtyping TeX in the first row of a lecture hall. When I started, my note-taking app of choice was Joplin. It lets you type Markdown notes with integrated TeX and a live preview - all while being open-source. After one semester though, I outgrew Joplin in favour of Obsidian. It basically does the same thing, but with a much larger community and faster active development - sadly, it is closed-source, which is the reason I took half a year to convince myself to make the switch.

In any case, both of those programs have Vim emulations - and none of them implement langmaps. Oh uh.


To use my note-taking app to its full extent, I had to find a suitable solution to my little issue. Immediately, I saw two possible paths:

  1. Try to create an Obsidian plugin that injects itself between my keyboard and CodeMirror.
  2. Go upstream and add langmap to CodeMirror.

What is CodeMirror you ask? Basically, whenever you see a code editor on a website or a webapp (as both Joplin and Obsidan are), chances are it's actually powered by CodeMirror - an open source code viewer and editor for the web.

Now, since Obsidian offloads the whole text editor part to an external project, it logically follows that the Vim emulation is also offsourced. Indeed, whenever you see a web-based code editor that happens to support Vim, chances are, the emulation is done by CodeMirror's child project - CodeMirror Vim.

Both the approaches have their advantages and disadvantages:

Fix in Obsidian Fix in CodeMirror Vim
Yay Live immediately: Since I'm just running additional code on my computer, it works as soon as I finish writing it. One fix to rule them all: Automatically add langmap to Obsidian, Joplin, and all similar apps
Nay Obsidian is not Open Source, so trying to inject code inside it would require some reverse-engineering and wouldn't be as robust as an upstream fix. The timeline of writing the fix, getting it approved, merging it into the repository, waiting for a new release, and waiting until Obsidian updates its dependencies... takes time.

So which direction did I choose? Of course both.

The Monkey Patch

The highest priority was getting my setup to work as soon as possible, so I can reliably take notes during my lectures. As such, coding a little plugin for Obsidian was the way to go.

Previously I mentioned that Obsidian is not open-source. While that's true, nothing prevents you from opening the Chromium developer tools and just look at its code that way - it is written in JavaScript after all. This fact enabled me to play reverse-engineering using the "web" browser's console.

The plan was as follows:

  1. Detect when Vim switches modes.
  2. Somehow switch our input mode from Dvorak to QWERTY or vice-versa.

Detecting Mode Change

At this point, I had never worked with CodeMirror before. I knew it was possible to detect what mode the Vim emulator is in, as the Vimrc Plugin does just that with a little colourful indicator. By looking through its source-code I was quickly able to determine how to listen for CodeMirror's Vim mode change.

As a side-note, we also need to detect when our app stops being in-focus and switch back to Dvorak, for example when I alt-tab into another program.

Switching the Layout

The next part would be slightly trickier. I could try to to override Obsidian's default key press events and run them through my middleware instead. In fact, I tried going down that path at first. The issue was reverse-engineering all the functions that my middleware would have to call after receiving a key event in order to restore Obsidian's functionality.

That approach was too time-intensive to go through with. Instead, I envisioned my Obsidian plugin communicating that mode change to my operating system, so that it could change the physical keyboard layout from Dvorak to QWERTY, or vice-versa. Sounds simple enough at first, until you learn that I use Windows on my laptop. I'd like to excuse that by saying this laptop's driver-availability for Linux is less than ideal (read: non-existent).

Windows does come with a Dvorak keyboard layout, but that's not what I'm using. You see, keyboard shortcuts for actions like Copying, Pasting, etc. are designed to be easy to press. Since Dvorak rearranges all the keys, Ctrl+C, Ctrl+V, Ctrl+X, Ctrl+S, and so on become a nightmare. Funnily enough, of all operating systems, MacOS is the one that gets this part right. It comes with a special kind of Dvorak layout that behaves like QWERTY upon pressing specific keys like Ctrl (or whatever the Apple word for that is). Thankfully, there exists a GitHub repository that brings that layout to Windows. I use a slightly modified version of it, which allows me to quickly type diacritics and special characters like ä, ö, ü, ß, and €.

With that out of the way, how do we switch layouts on the fly from some Javascript code? The part of communicating our intent is simple - we place an executable somewhere in the Obsidian plugin and execute it from the Javascript. But what do we do inside that executable?

HKL vs KLID

The Windows API is one of the worst-documented and messiest code bases I had the displeasure of working with. Evading the red herrings, we navigate our way to the correct message code. Upon posting that message with the HKL associated with the keyboard we want to use, the operating system switches to that layout. The funny thing is: The HKL is not unique to each installed keyboard layout.

Each keyboard layout is associated with two values. The KLID is unique to each layout and there exists a function to query the current keyboard's KLID. The HKL appears to be unique only to the language of the keyboard, but not the actual layout. But the only way to actually switch a keyboard layout is via the HKL!

This feels like a simple issue to circumvent. Windows actually lets you install "US QWERTY" layout for whatever language that you want, not just English. It's the exact same keyboard layout for all the languages, but their HKL is now different. Now, we simply leave our Dvorak layout as the English layout, and add another dummy language with US QWERTY layout. In my case I chose German. We should now be able to tell Windows to switch to English (aka Dvorak) or German (aka QWERTY). Not so fast!

Turns out, Windows simply refuses to switch to non-default keyboard layouts, regardless of what their HKL might seem to be. To reiterate, I don't use Windows' standard Dvorak layout. If I additionally install the default English Dvorak layout, our idea works. When I tell Windows to switch to English, it skips right over my layout and enables the newly-installed one. But that's not the one I want to be using.

AutoHotkey

Normally, to switch keyboard layouts, you use keyboard combinations like Win+Space. We can utilize that by adding yet another component to this madness. AutoHotkey, among other things, lets you create executables that emulate a pre-defined sequence of key presses. Using that, we could invoke the Win+Space or Win+Shift+Space combinations to switch to the next or the previous keyboard layout. And before you ask, no I could not use Windows API to do the same thing from my code, I tried.

If you remember what I said before about KLID, there theoretically exists a way to check which unique layout is currently being used. So we could simply cycle through all the layouts by repeatedly invoking the AutoHotkey binary, until the current layout matches what we wish to change to. By doing that, we wouldn't even have to invoke the whole HKL switch request in first place. Only if that system call actually worked. Which of course it doesn't. Querying the currently used keyboard layouts always returns the layout that was active when you started your program as per this still unresolved bug report. What's worse is that all the children of my process report the exact same layout, so I couldn't start yet another executable just to tell me what layout I'm currently on. As such, the only way to switch to my desired layout is by hard-coding how many times we have to run AutoHotkey.

Summary

To summarize the horrific setup that we've got running:

  1. We listen for Vim mode change.
  2. We start a keyboard switching binary.
  3. That binary requests a language switch from Windows.
  4. We now repeatedly start an AutoHotkey binary a hard-coded amount of times to move to the actual layout we desire.
  5. We hope that all of that happens with a lower-than-noticeable delay.

And that's what earned me the honour of being the "spacebar heating guy". As fragile as this setup sounds, it mostly works! The switch happens fast enough that I can get up to my usual Vim speed. There only remain a few minor issues:

  • When I press shift too fast when switching into insert mode, like when I want to type *, the shift key input gets lost during the keyboard change and I end up typing 8.
  • Sometimes the keyboard layout window will flicker on and off as AutoHotkey switches the layout.
  • When I change the program through the taskbar, my language switch request gets lost, so I must only switch using Alt-Tab. (Alt-Tabbing in itself is another funny edge case: I must wait until Alt is no longer being pressed before attempting to switch the layout, otherwise AutoHotkey will not change the layout, but simply cycle through my open windows.)

The finished product can be seen here.

The Upstream Fix

The quick and dirty solution works and got me through the semester, but it's far from perfect. As such, I also took upon adding language maps to CodeMirror Vim. The biggest hurdle was simply to learn and understand the existing code base. Once I got through that, it was simply the matter of utilizing the existing methods and interfaces. I like to make my pull requests as non-invasive as possible, so I only injected a function call to my new langmap logic at one critical point. Furthermore, I made sure that absolutely no remapping would be done if no langmap had been set and I also wrote test cases for the new feature.

As for the actual remapping - the biggest part is parsing the langmap format as per Vim's :help langmap. Once you've got that, remapping is as simple as doing a lookup in a hash map. Thankfully, I already had working parsing logic, further improved by a helpful collaborator from the repository.

Merging this into CodeMirror Vim was a joy. From the moment I opened the initial issue I was met with support from the aformentioned collaborator that stretched through the whole development process. The response times were short all throughout the ride and the whole experience left a great impression of this organization. Going forward from there, I still try to resolve other people's issues in that repository now and then, knowing I can rely on other contributors to aid me and not block the merges.

Waiting for my feature to make its way to the next release took a while, but it's now up and functional! All that's left to do is wait for Obsidian to update their dependences and voilà.

The Conclusion

My main take-away from this project is the importance of accessibility and keeping ears open to your users.

Everyone is different and we all experience the content out there differently. This stretches beyond Vim - think YouTube videos, websites, games, etc. While certain accessibility features may only impact a small percentile of the users, I believe it to be worth going the extra mile. In the grand scheme of things, it does not take much to add subtitles to a YouTube video, to add an aria-label to a website's button, or to add colour-blind mode to a game. On the other hand, the people at the receiving end will be thankful for your selflessness.

At the same time, I acknowledge the immense pressure that might put on solo developers. The more you think about you users, the more features you might think of adding - subtitles for the hearing-impaired, high-contrast setting for the vision-impaired, and so on. The thing about minorities is how many distinct ones there are. It's obvious you cannot please everyone. What you can however do, is keep your eyes, ears, and arms open to your community. I cannot express enough how happy I am with the maintainers over at CodeMirror Vim to have accepted my feauture without any hurdles. While it's a small gesture, it will have a big impact on me and other users in similar situations.

I mentioned I already had the majority of the logic ready to paste. In fact, I did already add a langmap to a Javascript-based project in the past. VSCodeVim enables Vim emulation in the text editor Visual Studio Code. While I mostly use the classic Vim for coding, I do like a more full-fledged editor or even an IDE for bigger projects. As you may have guessed, VSCodeVim does not support langmaps. The fully-implemented feature is there, ready to be merged. I've been using it off my own fork since completion and I am happy to say there are absolutely no issues with it. Unfortunately though, the pull request has been sitting there for over half a year and I don't think I'll live to see it finally added.

Again, I don't blame anyone there, but I would urge looking over to you user-base every now in a while. If you don't have the time or interest to resolve pending issues, consider adding new maintainers to the repository - in open source, we're all in this together. But it is a pity to let fully-working features rot indefinitely.