Custom mechanical keyboards have long been split into two camps: Wired keyboards running the open source QMK firmware, and closed source Bluetooth keyboards running a proprietary firmware. Bluetooth support introduces all sorts of messy power requirements, licensing issues, and other technical overhead that historically hasn’t meshed well with the barebones Arduino Pro Micro / ATmega32U4 infrastructure that powers the vast majority of QMK keyboards. There were always high-powered alternatives like the ESP32 microcontroller, but none of them really made inroads into the hobbyist keyboard space.

That is until the Nice!Nano burst onto the scene. It features a Nordic nRF52840 microcontroller with native Bluetooth support, and is low power enough to work in wireless keyboard contexts. The Nice!Nano shares a footprint with the Arduino Pro Micro, meaning it can be used by many of the existing DIY keyboard PCBs.

The one thing it couldn’t take advantage of however is QMK. The popular open source keyboard firmware does not have support for wireless connectivity, and most likely won't be adding it in the future due to a variety of architectural and licensing issues.. This has left wireless enthusiasts to turn to the alternative open source ZMK keyboard firmware. ZMK was built from the ground up with wireless support in mind, and philosophically very similar to QMK. It’s still a relatively new project however, so not every QMK feature has a direct ZMK equivalent.

If you use a ZMK keyboard on multiple operating systems, one such gap you may have run into is the ability to quickly switch your keyboard's hotkeys to match your operating system's paradigm.

OS Hotkey Differences

When Apple and IBM originally designed the keyboards for their systems, they made slightly different decisions when it came to the nomenclature and layout of keyboard modifier keys. IBM (and later every Window-based system) would use Control, Windows, and Alt, while Apple settled on Control, Option, and Command. These keys all send the same codes to the computer, Apple just swaps Alt for Opt, and Win for Cmd. In operating system agnostic instances (or when chatting with Linux folks), Win will sometimes be substituted for GUI or Super.

Besides the names, there's two small differences between systems. Windows machines order their modifier keys Ctrl, Win, Alt, while Macs order their keys Ctrl, Opt, Cmd. This isn't just a slight rejiggering of lesser-used commands either. On Windows machines, Ctrl is the primary hotkey for most common system tasks. On Macs, it's the Cmd key. So "Undo" on a Windows PC is Ctrl+Z, while on a Mac it's Cmd+Z.

Windows users rely on their pinkies for most shortcuts, while Mac users rely on their thumbs. Using Windows (somehow) on a MacBook keyboard is a non-issue, but trying to use MacOS on a Windows keyboard is a terrible experience. The muscle memory for all of your basic hotkeys is broken, and your thumb needs to make an awkward stretch over to the Windows key. It's not a great time.

To rectify this, most keyboard manufactures include some sort of software option or toggle switch which remaps the position of Win and Alt on keyboards. This is generally a great solution, although Windows users will inevitably mix up Cmd and Opt on the rare time they need to use Alt or Win on a Mac-style keyboard.

On ergonomic keyboards like the Sofle Choc I recently reviewed, you'll often find a slightly different situation. These keyboards will typically try to locate several useful modifier keys around the natural resting place for their thumb. If you're on Windows, you set it to Ctrl. If you're on MacOS, you set it to Cmd. If you're regularly switching between different operating systems, it's vital that you have some way of changing the modifier triggered by these thumb keys.

OS Hotkey Switching in QMK

QMK offers exactly this sort of hotkey-switching functionality. Both QMK and ZMK allow users to completely customize their keyboard's layout. If you know you’re going to be spending all of your time on one operating system, you can simply flash a Windows or MacOS style layout to your keyboard. If you’re regularly working with multiple operating systems on the same keyboard however, you need some way of toggling your modifier layout. QMK of course solves this through, what else, a hotkey.

QMK has a set of what it calls “Magic Keycodes” that can alter the current state of a keyboard’s setup. The MAGIC_TOGGLE_ALT_GUI (AG_TOGG) keycode switches the position of Alt and Win/Cmd. Meanwhile, the MAGIC_TOGGLE_CTL_GUI (CG_TOGG) keycode switches the position of Ctrl and Win/Cmd for ergonomic keyboards. Once you've assigned these keycodes to a key on your keyboard (or assigned it to a hotkey combination on a layer), all you need to do is tap the key and it will seamlessly toggle between these two behaviors.

These magic keycodes also come in “Swap” and “Unswap” varieties. Instead of toggling back and forth, these commands allow you to create dedicated “MacOS” or “Windows” hotkeys on your keyboard. The toggle behavior is usually enough for basic setups, by the swap and unswap behaviors can be useful for including in macros.

Overall it’s a nice straightforward piece of functionality. If you’re swapping systems frequently, working with cross-OS virtual machines, or using a KVM, these magic keycodes help keep your keyboard in line with your operating system. If your keyboard has a display, you can even reference its current state on the display.

OS Hotkey Switching in ZMK

ZMK currently does not have any out of the box support for hotkey switching. Thankfully it’s fairly easy to fake QMK’s magic keycode toggle behavior. What we’d like to do is be able to have the keyboard provide one set of modifier behavior, tap a single keycode, and then have the keyboard exhibit a different set of modifier behavior from that point forward. We can accomplish this by getting a little creative with layers.

keymap {
  compatible = "zmk,keymap";

  default_layer {
    bindings = <
      &kp GRAVE &kp N1 &kp N2    &kp N3   &kp N4   &kp N5                   &kp N6 &kp N7   &kp N8    &kp N9    &kp N0   &kp MINUS
      &kp ESC   &kp Q  &kp W     &kp E    &kp R    &kp T                    &kp Y  &kp U    &kp I     &kp O     &kp P    &kp BSPC
      &kp TAB   &kp A  &kp S     &kp D    &kp F    &kp G                    &kp H  &kp J    &kp K     &kp L     &kp SEMI &kp SQT
      &kp LSHFT &kp Z  &kp X     &kp C    &kp V    &kp B                    &kp N  &kp M    &kp COMMA &kp DOT   &kp FSLH &kp RSHFT
                       &kp LCTRL  &kp LALT &kp LGUI &mo 1  &kp RET &kp SPACE &mo 2  &kp RGUI &kp RALT  &kp RCTRL
    >;
  };

  lower_layer {
    bindings = <
      &trans &kp F1    &kp F2    &kp F3      &kp F4   &kp F5                 &kp F6    &kp F7   &kp F8    &kp F9    &kp F10  &kp F11
      &trans &kp N1    &kp N2    &kp N3      &kp N4   &kp N5                 &kp N6    &kp N7   &kp N8    &kp N9    &kp N0   &kp F12
      &trans &kp EXCL  &kp AT    &kp HASH    &kp DLLR &kp PRCNT              &kp CARET &kp AMPS &kp ASTRK &kp LPAR  &kp RPAR &kp PIPE
      &trans &kp EQUAL &kp MINUS &kp KP_PLUS &kp LBRC &kp RBRC               &kp LBKT  &kp RBKT &kp SEMI  &kp COLON &kp BSLH &trans
                       &trans    &trans      &trans   &trans   &trans &trans &mo 3     &trans   &trans    &trans
    >;
  };

  upper_layer {
    bindings = <
      &trans &trans  &trans    &trans      &trans &trans                 &trans    &trans   &trans   &trans    &trans &trans
      &trans &kp INS &kp PSCRN &kp K_CMENU &trans &kp SLCK               &kp PG_UP &trans   &kp UP   &trans    &trans &kp DEL
      &trans &trans  &trans    &trans      &trans &kp CLCK               &kp PG_DN &kp LEFT &kp DOWN &kp RIGHT &trans &trans
      &trans &trans  &trans    &trans      &trans &trans                 &trans    &kp HOME &trans   &kp END   &trans &trans
                     &trans    &trans      &trans &mo 3    &trans &trans &trans    &trans   &trans   &trans
    >;
  };

  mod_layer {
    bindings = <
      &bt BT_CLR  &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4               &trans &trans       &trans     &trans       &trans &trans
      &kp K_SLEEP &mac_bt      &win_bt      &trans       &trans       &trans                     &trans &trans       &trans     &trans       &trans &trans
      &tog 1      &trans       &trans       &trans       &trans       &trans                     &trans &kp C_VOL_DN &kp C_MUTE &kp C_VOL_UP &trans &trans
      &trans      &trans       &trans       &trans       &trans       &trans                     &trans &kp C_PREV   &kp C_PP   &kp C_NEXT   &trans &trans
                               &trans       &trans       &trans       &trans       &trans &trans &trans &trans       &trans     &trans
    >;
  };
};
A standard Mac-specific layout.

For this example, let’s assume we’ve got a fairly standard multi-layer setup. The base layer is a straightforward QWERTY layer, then the user has a Symbol layer for special characters, a Navigation layer for arrow keys, and an Adjustment layer for things like managing Bluetooth profiles.

In this example, the user has set their keyboard with a Mac-style Ctrl, Opt, Cmd layout by default. First, let’s add a Windows-specific layer. Copy your base QWERTY layer and swap the Ctrl and Cmd hotkeys.

keymap {
  compatible = "zmk,keymap";

  default_layer {
    bindings = <
      &kp GRAVE &kp N1 &kp N2    &kp N3   &kp N4   &kp N5                   &kp N6 &kp N7   &kp N8    &kp N9    &kp N0   &kp MINUS
      &kp ESC   &kp Q  &kp W     &kp E    &kp R    &kp T                    &kp Y  &kp U    &kp I     &kp O     &kp P    &kp BSPC
      &kp TAB   &kp A  &kp S     &kp D    &kp F    &kp G                    &kp H  &kp J    &kp K     &kp L     &kp SEMI &kp SQT
      &kp LSHFT &kp Z  &kp X     &kp C    &kp V    &kp B                    &kp N  &kp M    &kp COMMA &kp DOT   &kp FSLH &kp RSHFT
                       &kp LCTRL  &kp LALT &kp LGUI &mo 2  &kp RET &kp SPACE &mo 3  &kp RGUI &kp RALT  &kp RCTRL
    >;
  };
      
  win_layer {
    bindings = <
      &kp GRAVE &kp N1 &kp N2   &kp N3   &kp N4    &kp N5                   &kp N6 &kp N7    &kp N8    &kp N9    &kp N0   &kp MINUS
      &kp ESC   &kp Q  &kp W    &kp E    &kp R     &kp T                    &kp Y  &kp U     &kp I     &kp O     &kp P    &kp BSPC
      &kp TAB   &kp A  &kp S    &kp D    &kp F     &kp G                    &kp H  &kp J     &kp K     &kp L     &kp SEMI &kp SQT
      &kp LSHFT &kp Z  &kp X    &kp C    &kp V     &kp B                    &kp N  &kp M     &kp COMMA &kp DOT   &kp FSLH &kp RSHFT
                       &kp LGUI &kp LALT &kp LCTRL &mo 2  &kp RET &kp SPACE &mo 3  &kp RCTRL &kp RALT  &kp RGUI
    >;
  };

  lower_layer {
    bindings = <
      &trans &kp F1    &kp F2    &kp F3      &kp F4   &kp F5                 &kp F6    &kp F7   &kp F8    &kp F9    &kp F10  &kp F11
      &trans &kp N1    &kp N2    &kp N3      &kp N4   &kp N5                 &kp N6    &kp N7   &kp N8    &kp N9    &kp N0   &kp F12
      &trans &kp EXCL  &kp AT    &kp HASH    &kp DLLR &kp PRCNT              &kp CARET &kp AMPS &kp ASTRK &kp LPAR  &kp RPAR &kp PIPE
      &trans &kp EQUAL &kp MINUS &kp KP_PLUS &kp LBRC &kp RBRC               &kp LBKT  &kp RBKT &kp SEMI  &kp COLON &kp BSLH &trans
                       &trans    &trans      &trans   &trans   &trans &trans &mo 4     &trans   &trans    &trans
    >;
  };

  upper_layer {
    bindings = <
      &trans &trans  &trans    &trans      &trans &trans                 &trans    &trans   &trans   &trans    &trans &trans
      &trans &kp INS &kp PSCRN &kp K_CMENU &trans &kp SLCK               &kp PG_UP &trans   &kp UP   &trans    &trans &kp DEL
      &trans &trans  &trans    &trans      &trans &kp CLCK               &kp PG_DN &kp LEFT &kp DOWN &kp RIGHT &trans &trans
      &trans &trans  &trans    &trans      &trans &trans                 &trans    &kp HOME &trans   &kp END   &trans &trans
                     &trans    &trans      &trans &mo 4    &trans &trans &trans    &trans   &trans   &trans
    >;
  };

  mod_layer {
    bindings = <
      &bt BT_CLR  &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4               &trans &trans       &trans     &trans       &trans &trans
      &kp K_SLEEP &mac_bt      &win_bt      &trans       &trans       &trans                     &trans &trans       &trans     &trans       &trans &trans
      &tog 1      &trans       &trans       &trans       &trans       &trans                     &trans &kp C_VOL_DN &kp C_MUTE &kp C_VOL_UP &trans &trans
      &trans      &trans       &trans       &trans       &trans       &trans                     &trans &kp C_PREV   &kp C_PP   &kp C_NEXT   &trans &trans
                               &trans       &trans       &trans       &trans       &trans &trans &trans &trans       &trans     &trans
    >;
  };
};
An enhanced keymap that allows you to swap Cmd and Ctrl for a Windows-specific layout.

At this point, you should have the following layers:

  • Layer 0: Mac
  • Layer 1: Windows
  • Layer 2: Symbol
  • Layer 3: Navigation
  • Layer 4: Adjust

Go ahead and update your Raise and Lower keys to skip over the Windows layer for now. The left thumb should go straight to Layer 2, and your right thumb should go to Layer 3. They're the &mo 2 and &mo 3 commands you see in the center of each keymap.

At this point, your keyboard should be working as it usually does. The Windows layer exists, but it’s being completely ignored. The Symbol, Nav, and Adjust layers are being applied on top of your Mac layer.

So what we need to do now is somehow swap out the base Layer 0 Mac layer with the Layer 1 Windows layer. Thankfully, ZMK has support for this. Its “Toggle Layer” keycode enables a layer until the layer is manually disabled. Critically however, this still allows you to use the standard “Momentary Layer” behavior to get to your Symbol, Nav, and Adjust layers.

Add a &tog 1 keycode to an unused key on your keymap. The `Tab` key on the Adjust layer is usually a good candidate. (You can see I already went ahead and did that on the earlier code embed.)

Now if you press Raise+Lower+Tab, your base layer will shift from Layer 0 to Layer 1. This can be used for much more advanced keyboard configurations (shifting from Qwerty to Colemak for instance), but in this case the only change we made between these two layers is the order of the modifier keys. Presto, we're now using Ctrl as our primary modifier instead of Cmd. The thumb keys are still configured to &mo 2 and &mo 3, so we can still enable the Symbol, Nav, and Adjust layers. To switch back to Mac-style keys, simply press Raise+Lower+Tab again.

This approach does have a few limitations over QMK. First and foremost, the keyboard will reset back to its default layer when you restart or unplug the device. On QMK, the magic keycodes persist after power cycling. Additionally, you need to create a unique layer to &tog to for each specific behavior you’re looking to implement. If you wanted to simulate bothAG_TOGG and CG_TOGG simultaneously, your layer definitions will start to balloon significantly.

Integrating OS Hotkey Switching With ZMK Bluetooth Profiles

This Raise+Lower+Tab approach works well for situations like virtual machines and KVMs, but what if you’re actually using the bluetooth capabilities of ZMK to switch between two physical Mac and Windows devices? You can press ZMK's &bt BT_SEL keycode and then our &tog command individually, but ZMK's macro capabilities can easily automate this process.

macros {
  mac_bt: mac_bt {
    label = "mac_bt";
    compatible = "zmk,behavior-macro";
    #binding-cells = <0>;
    bindings
      = <&macro_tap &bt BT_SEL 0>
      , <&macro_tap &tog 0>
    ;
  };
  
  win_bt: win_bt {
    label = "win_bt";
    compatible = "zmk,behavior-macro";
    #binding-cells = <0>;
    bindings
      = <&macro_tap &bt BT_SEL 1>
      , <&macro_tap &tog 1>
    ;
  };
};
Macro definitions for switching layouts and devices at the same time.

Define a pair of macros at the top of your keymap file. Assuming your Mac is on BT 0 and your Windows machine is on BT1, you should easily be able to create a mac_bt and win_bt pair of macros to switch to each device. We'll use <&macro_tap> to activate the same exact keycodes as before.

You can replace the original Bluetooth profile hotkeys on your keymap with these macros, but my Adjust layer had plenty of spare keys for them. It's easy enough to slot the macro-version of those commands underneath the original keys. You can call the macros with &mac_bt or &win_bt. (Again, I didn't bother hiding these from our enhanced keymap up above.) You may still want to keep around the original OS toggle hotkey on Tab just in case things get out of sync.

Conclusion

In the absence of official ZMK support for OS modifier switching, these layer tricks should help you straddle the world between operating systems. Hope this helped someone in a similar situation to me!