I Taught Windows To Speak Mac, One Scancode At A Time
Three weeks into a Windows box, I killed a running dev server by pressing Cmd+C to copy a stack trace.
I had done the thing every Mac-refugee guide tells you to do: swap Cmd and Ctrl. That is exactly why the server died. The moment you remap the Cmd-position key to Ctrl, you have welded the copy key and the kill key into the same piece of plastic. Every guide that tells you to make that swap stops writing right about there.
This is the rest of that post. Full config is in a gist if you want to skip ahead.
Three Layers, And Don’t Mix Them
- The driver layer. One physical key becomes another. Global, invisible to every application. Modifiers only.
- AutoHotkey. Combinations become other combinations.
Cmd+LeftbecomesHome. Every multi-key shortcut lives here. - Apps that were already fine. Once the driver says your Cmd key is Ctrl, a lot of software speaks Mac with no configuration at all.
The rule that keeps this stable: the driver layer cannot do combinations, and AutoHotkey should not do modifiers. Swap modifiers in AutoHotkey and you get sticky keys during drags and phantom modifiers in games. Respect the split and it runs for months untouched.
Layer One: The Scancode Map
Windows reads a registry value called Scancode Map in the keyboard class driver, at boot. It sits below applications, below input hooks, below anti-cheat, below the login screen. Nothing in userspace can tell a remapped key from a real one.
SharpKeys is a GUI for writing it, but the format is worth knowing:
00,00,00,00,00,00,00,00, <- header
04,00,00,00, <- mapping count (3 mappings + terminator)
1d,e0,3a,00, <- Caps Lock (0x3A) -> RIGHT Ctrl (0xE01D)
5b,e0,1d,00, <- Left Ctrl (0x1D) -> Left Win (0xE05B)
1d,00,5b,e0, <- Left Win (0xE05B) -> LEFT Ctrl (0x1D)
00,00,00,00 <- terminator
Each mapping is four bytes: the key you want, then the key you have, both little-endian. That reversed order catches everyone once.
Those three lines are the whole macOS modifier arrangement. The key where Cmd sits now emits Ctrl, so Cmd+C, Cmd+S, Cmd+T, Cmd+W, Cmd+L and several dozen other reflexes start working system-wide with no further config.
Line three matters more than it looks. Caps Lock becomes right Ctrl, not left. That asymmetry is what makes the terminal fix possible later.
1Windows Registry Editor Version 5.00
2
3[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout]
4"Scancode Map"=hex:00,00,00,00,00,00,00,00,04,00,00,00,1d,e0,3a,00,5b,e0,1d,00,1d,00,5b,e0,00,00,00,00
Import with admin rights and reboot. The reboot is not optional, the driver reads this once at boot. Note that it applies to every keyboard attached to the machine, forever.
Layer Two: AutoHotkey
macOS has a system-wide text editing layer. Every text field in every app inherits the same navigation from Cocoa: Cmd+Left for line start, Opt+Left for previous word, Opt+Delete to kill a word. Windows has no equivalent, so you rebuild it:
1#Requires AutoHotkey v2.0+
2#SingleInstance Force
3
4; Cmd+arrows: line and document
5^Left::SendInput("{Home}")
6^Right::SendInput("{End}")
7^Up::SendInput("^{Home}")
8^Down::SendInput("^{End}")
9
10; ...with Shift, for selection
11^+Left::SendInput("+{Home}")
12^+Right::SendInput("+{End}")
13^+Up::SendInput("^+{Home}")
14^+Down::SendInput("^+{End}")
15
16; Opt+arrows: by word
17!Left::SendInput("^{Left}")
18!Right::SendInput("^{Right}")
19!+Left::SendInput("^+{Left}")
20!+Right::SendInput("^+{Right}")
21
22; Opt+Backspace: kill previous word
23!Backspace::SendInput("^{Backspace}")
24
25; Cmd+Q
26^q::SendInput("!{F4}")
27
28; Tab switching, both chords macOS gives you
29^!Left::SendInput("^{PgUp}") ; Cmd+Opt+Left
30^!Right::SendInput("^{PgDn}") ; Cmd+Opt+Right
31^+[::SendInput("^{PgUp}") ; Cmd+Shift+[
32^+]::SendInput("^{PgDn}") ; Cmd+Shift+]
After layer one, ^ in AutoHotkey means the key where Cmd lives. Read these as Cmd+Left, Opt+Left, Cmd+Q.
Tab switching gets both macOS chords pointed at the same Windows behaviour, so whichever one your fingers trained on works. It cannot be Ctrl+Tab, for a reason worth knowing: once you swap Ctrl and Win, a literal Ctrl+Tab is untypeable, because the key that used to send Ctrl now sends Win. Anything that genuinely required Ctrl is gone, and you route around it.
(Ctrl+Shift+[ and ] are fold and unfold in VS Code on Windows. Taking them is arguably the right call, since on macOS those brackets switch editor tabs there and folding lives on Cmd+K Cmd+[, but scope them out if you fold by habit.)
If your keyboard is ISO rather than ANSI, the key left of 1 may type a backslash, since ISO boards put grave down by the left Shift:
1SC056::SC029
Use a::b remap syntax rather than sending characters. Remaps run at send level zero so they cannot retrigger your own hotkeys, and Shift passes through automatically, which gives you ~ without a second line.
The Terminal Fix
In a terminal, Cmd+C must copy and Ctrl+C must send SIGINT. After layer one they are the same key. This costs you half-written prompts and long builds, daily.
The usual workaround is a conditional binding: copy if there is a selection, otherwise pass through ^C. It works at a bare shell prompt and fails in any full-screen TUI, because those enable mouse reporting, so the terminal never owns a selection and every press takes the ^C branch.
The fix is to stop asking the terminal. Terminals cannot distinguish left Ctrl from right Ctrl (WezTerm’s modifier vocabulary is just CTRL, and left/right support is still an unmerged PR). AutoHotkey can: <^ matches left Ctrl only, >^ matches right Ctrl only. That is useless while both keys emit the same scancode, which is why Caps Lock went to right Ctrl back in layer one.
1#HotIf WinActive("ahk_exe wezterm-gui.exe")
2<^c::SendInput("{Blind}{Shift down}c{Shift up}")
3#HotIf
Inside the terminal, left Ctrl plus C (your Cmd key) gets a Shift added and arrives as Ctrl+Shift+C, the terminal’s native copy binding. Right Ctrl plus C (your Caps Lock) is not matched and reaches the pty as a real ^C.
Cmd+C copies. Caps+C interrupts. No heuristics, no failure mode in full-screen apps.
{Blind} is load-bearing: it leaves the held Ctrl alone instead of releasing and re-pressing it, which would desync your modifier state on key repeat.
Two portability notes. The mechanism works anywhere, but the Ctrl+Shift+C payload is a convention: fine in WezTerm, Windows Terminal, mintty and conhost, but not VS Code, where Ctrl+Shift+C opens an external terminal. VS Code’s integrated terminal implements its own copy-if-selection behaviour anyway, so leave it out of the scope.
Layer Three: Apps That Are Already Fine
Once your Cmd key emits Ctrl, anything that lets you bind a Ctrl shortcut is already speaking Mac.
- AltAppSwitcher gives you a real
Cmd+Tab: switches applications rather than windows, holds open while you tap, cycles windows onCmd+backtick. Set its hold key to left control. Windows’ native Alt+Tab is window-based, which is a behaviour problem, not a binding one. - ShareX for screenshots. Bind region capture to
Ctrl+Shift+4, full screen toCtrl+Shift+3, recording toCtrl+Shift+5. Read those with your Cmd key in mind: that is macOS screenshots exactly, numbers included, with no scripting. - Flow Launcher is Spotlight.
- QuickLook brings back tapping Space on a file to preview it.
Four installs, no code.
Finder Reflexes
Explorer needs a small scoped block, because a few Finder gestures are exactly backwards on Windows:
1#HotIf WinActive("ahk_class CabinetWClass")
2^Up::SendInput("!{Up}") ; Cmd+Up: up a folder
3^Delete::SendInput("{Delete}") ; Cmd+Delete: move to trash
4^i::SendInput("!{Enter}") ; Cmd+I: get info
5#HotIf
Cmd+Delete is the one that stings. In Finder, plain Delete does nothing and Cmd+Delete trashes. On Windows it is the reverse.
The scoping is not cosmetic. Bind Ctrl+I globally and you trade italic in every editor for one Finder shortcut. Bind Ctrl+Delete globally and you lose delete-word-forward everywhere.
What You Don’t Get Back
- Finder’s column view. Explorer has nothing like it.
- Press-and-hold for accented characters. If you write with diacritics, that becomes explicit AutoHotkey bindings, one line per character.
- System-wide spell check in every text field.
Ctrl+Tab, sacrificed to the modifier swap.Cmd+Qmeaning “quit the application.”Alt+F4closes the window, not the app.
Everything else on this page, I got back.
One last thing: comment your keyboard config. In eight months the thing you need is not the mapping, it is the reason for the mapping. Future You will find SC056::SC029 sitting there unexplained, delete it, and spend an afternoon rediscovering that the key left of 1 types a backslash.
Drafted with an LLM and edited by hand, as usual. The setup is real and running on the machine I typed this on.