Last active
March 12, 2026 23:08
-
-
Save derek-shnosh/13c82872da665c32d181340c9a45e7ad to your computer and use it in GitHub Desktop.
Windows Keybinding config for MacOS 26 using Hammerspoon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ---@diagnostic disable-next-line: lowercase-global | |
| hs = hs | |
| local eventtap = hs.eventtap | |
| local eventTypes = eventtap.event.types | |
| local keycodes = hs.keycodes.map | |
| local mouseEvent = hs.eventtap.event | |
| local mouseEventTypes = mouseEvent.types | |
| local mouseEventProperties = mouseEvent.properties | |
| print("Hammerspoon: Loading Windows Shortcuts...") | |
| -- SKIPPED APPS | |
| SKIPPED_BUNDLE_IDS = { | |
| ["org.virtualbox.app.VirtualBoxVM"] = true, | |
| ["com.parallels.desktop.console"] = true, | |
| ["org.vmware.fusion"] = true, | |
| ["org.gnu.emacs"] = true, | |
| ["org.gnu.Emacs"] = true, | |
| ["com.jetbrains"] = true, | |
| ["com.vscodium"] = true, | |
| ["com.sublimetext.3"] = true, | |
| ["net.kovidgoyal.kitty"] = true, | |
| ["com.ScooterSoftware"] = true, | |
| ["dev.zed.Zed"] = true, | |
| ["com.citrix.XenAppViewer"] = true, | |
| ["com.microsoft.rdc.macos"] = true, | |
| ["com.googlecode.iterm2"] = true, | |
| ["com.apple.Terminal"] = true, | |
| ["com.github.wez.wezterm"] = true, | |
| ["com.mitchellh.ghostty"] = true, | |
| } | |
| -- KEY MAPS | |
| KEY_MAPS = { | |
| -- Navigation | |
| ["home"] = "cmd+left", | |
| ["shift+home"] = "cmd+shift+left", | |
| ["ctrl+home"] = "cmd+up", | |
| ["ctrl+shift+home"] = "cmd+shift+up", | |
| ["end"] = "cmd+right", | |
| ["shift+end"] = "cmd+shift+right", | |
| ["ctrl+end"] = "cmd+down", | |
| ["ctrl+shift+end"] = "cmd+shift+down", | |
| -- Words / Deletion | |
| ["ctrl+left"] = "option+left", | |
| ["ctrl+shift+left"] = "option+shift+left", | |
| ["ctrl+right"] = "option+right", | |
| ["ctrl+shift+right"] = "option+shift+right", | |
| ["ctrl+delete"] = "option+delete", | |
| ["ctrl+forwarddelete"] = "option+forwarddelete", | |
| -- Standard Commands | |
| ["ctrl+a"] = "cmd+a", | |
| ["ctrl+b"] = "cmd+b", | |
| ["ctrl+c"] = "cmd+c", | |
| ["ctrl+d"] = "cmd+d", | |
| ["ctrl+e"] = "cmd+e", | |
| ["ctrl+f"] = "cmd+f", | |
| ["ctrl+g"] = "cmd+g", | |
| ["ctrl+h"] = "cmd+h", | |
| ["ctrl+i"] = "cmd+i", | |
| ["ctrl+j"] = "cmd+j", | |
| ["ctrl+k"] = "cmd+k", | |
| ["ctrl+l"] = "cmd+l", | |
| ["ctrl+m"] = "cmd+m", | |
| ["ctrl+n"] = "cmd+n", | |
| ["ctrl+o"] = "cmd+o", | |
| ["ctrl+p"] = "cmd+p", | |
| ["ctrl+q"] = "cmd+q", | |
| ["ctrl+r"] = "cmd+r", | |
| ["ctrl+s"] = "cmd+s", | |
| ["ctrl+t"] = "cmd+t", | |
| ["ctrl+u"] = "cmd+u", | |
| ["ctrl+v"] = "cmd+v", | |
| ["ctrl+w"] = "cmd+w", | |
| ["ctrl+x"] = "cmd+x", | |
| -- ["ctrl+y"] = "cmd+y", -- ignored in favor of "Redo" mapping | |
| ["ctrl+z"] = "cmd+z", | |
| ["ctrl+,"] = "cmd+,", | |
| ["ctrl+/"] = "cmd+/", | |
| ["ctrl+="] = "cmd+=", | |
| ["ctrl+-"] = "cmd+-", | |
| -- "Redo" | |
| ["ctrl+y"] = "cmd+shift+z", | |
| -- Command Palette / App Actions | |
| ["ctrl+shift+p"] = "cmd+shift+p", | |
| -- App Management | |
| ["option+tab"] = "cmd+tab", | |
| ["option+f4"] = "cmd+w", | |
| } | |
| local function getHotkeyString(event) | |
| local code = event:getKeyCode() | |
| local flags = event:getFlags() | |
| local name = keycodes[code] | |
| if not name then return nil end | |
| local hotkey = "" | |
| if flags.ctrl then hotkey = hotkey .. "ctrl+" end | |
| if flags.cmd then hotkey = hotkey .. "cmd+" end | |
| if flags.alt then hotkey = hotkey .. "option+" end | |
| if flags.shift then hotkey = hotkey .. "shift+" end | |
| return hotkey .. name | |
| end | |
| local function eventHandler(event) | |
| -- Use frontmostApplication so logic works even when no windows are open | |
| local activeApp = hs.application.frontmostApplication() | |
| if not activeApp then return false end | |
| local bundleID = activeApp:bundleID() or "" | |
| local hotkey = getHotkeyString(event) | |
| if not hotkey then return false end | |
| -- GLOBALS (Works everywhere, even skipped apps) | |
| ------------------------------------------------------------------------------ | |
| -- Global Cmd+E to open a new Finder window | |
| if hotkey == "cmd+e" then | |
| hs.applescript.applescript([[ | |
| tell application "Finder" | |
| make new Finder window to home | |
| activate | |
| end tell | |
| ]]) | |
| return true | |
| end | |
| -- Global Ctrl+Opt+T to Open iTerm2 | |
| if hotkey == "ctrl+option+t" then | |
| hs.application.launchOrFocusByBundleID("com.googlecode.iterm2") | |
| return true | |
| end | |
| -- Global Cmd+Opt+I to open System Settings | |
| if hotkey == "cmd+option+i" then | |
| hs.application.launchOrFocusByBundleID("com.apple.systempreferences") | |
| return true | |
| end | |
| -- APP-SPECIFIC EXCEPTIONS (iTerm2, Finder, etc.) | |
| ------------------------------------------------------------------------------ | |
| -- iTerm2 Mappings (Exceptions) | |
| -- Manually bridge specific "Windows-style" system shortcuts to keep native | |
| -- terminal Ctrl-commands because iTerm2 is excluded from global remapping | |
| if bundleID == "com.googlecode.iterm2" then | |
| -- Map Ctrl+Q to Cmd+Q (Quit App) | |
| if hotkey == "ctrl+q" then | |
| hs.eventtap.keyStroke({ "cmd" }, "q", 0) | |
| return true | |
| end | |
| -- Map Ctrl+ to Cmd+ (Open iTerm2 Preferences) | |
| if hotkey == "ctrl+," then | |
| hs.eventtap.keyStroke({ "cmd" }, ",", 0) | |
| return true | |
| end | |
| -- Map Alt+F4 to Cmd+W (Close current Tab/Window) | |
| if hotkey == "option+f4" then | |
| hs.eventtap.keyStroke({ "cmd" }, "w", 0) | |
| return true | |
| end | |
| -- Map Ctrl+=, Ctrl+-, and Ctrl+0 (Zoom text) | |
| if hotkey == "ctrl+=" then | |
| hs.eventtap.keyStroke({ "cmd", "shift" }, "=", 0) | |
| return true | |
| end | |
| if hotkey == "ctrl+-" then | |
| hs.eventtap.keyStroke({ "cmd" }, "-", 0) | |
| return true | |
| end | |
| if hotkey == "ctrl+0" then | |
| hs.eventtap.keyStroke({ "cmd" }, "0", 0) | |
| return true | |
| end | |
| end | |
| -- Finder Mappings | |
| if bundleID == "com.apple.finder" then | |
| -- Windows 'Delete' -> Mac 'Move to Trash' | |
| if hotkey == "forwarddelete" then | |
| hs.eventtap.keyStroke({ "cmd" }, "delete", 0) | |
| return true | |
| end | |
| -- Windows 'Shift+Delete' -> Mac 'Delete Immediately' | |
| if hotkey == "shift+forwarddelete" then | |
| hs.eventtap.keyStroke({ "cmd", "option" }, "delete", 0) | |
| return true | |
| end | |
| -- Windows 'Alt+Left' -> Mac 'Go to Parent Folder' | |
| if hotkey == "option+left" then | |
| hs.eventtap.keyStroke({ "cmd" }, "up", 0) | |
| return true | |
| end | |
| -- Windows 'Alt+Right' -> Mac 'Open Folder/File' | |
| if hotkey == "option+right" then | |
| hs.eventtap.keyStroke({ "cmd" }, "down", 0) | |
| return true | |
| end | |
| end | |
| -- GLOBAL SKIP LIST (Blocks the generic remapping below) | |
| ------------------------------------------------------------------------------ | |
| if SKIPPED_BUNDLE_IDS[bundleID] then return false end | |
| -- STANDARD REMAPPING LOGIC (Windows Style) | |
| ------------------------------------------------------------------------------ | |
| local mapped_hotkey = KEY_MAPS[hotkey] | |
| if mapped_hotkey then | |
| local parts = hs.fnutils.split(mapped_hotkey, "+") | |
| local key = table.remove(parts) | |
| local mods = {} | |
| for _, mod in ipairs(parts) do | |
| if mod == "option" then mod = "alt" end | |
| table.insert(mods, mod) | |
| end | |
| hs.eventtap.keyStroke(mods, key, 0) | |
| return true | |
| end | |
| return false | |
| end | |
| -- Global reference to keep the tap alive | |
| SHORTCUT_TAP = hs.eventtap.new({ eventTypes.keyDown }, eventHandler):start() | |
| MOUSE_NAV_BUTTONS_TAP = hs.eventtap.new({ mouseEventTypes.otherMouseDown }, function(mouseDownEvent) | |
| local buttonNumber = mouseDownEvent:getProperty(mouseEventProperties.mouseEventButtonNumber) | |
| if buttonNumber == 3 then | |
| hs.eventtap.keyStroke({ "cmd" }, "[", 0); return true | |
| end | |
| if buttonNumber == 4 then | |
| hs.eventtap.keyStroke({ "cmd" }, "]", 0); return true | |
| end | |
| return false | |
| end):start() | |
| -- Hot-Reload config in hammerspoon when saving init.lua | |
| local function reloadConfig(paths) | |
| for _, file in pairs(paths) do | |
| if file:sub(-4) == ".lua" then | |
| hs.reload() | |
| return | |
| end | |
| end | |
| end | |
| hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start() | |
| print("Hammerspoon: Ready.") |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Accompanying VScode key-mappings
[ { "key": "cmd+c", "command": "workbench.action.terminal.sendSequence", "when": "terminalFocus", "args": { "text": "\u0003" }, }, { "key": "cmd+h", "command": "editor.action.startFindReplaceAction", "when": "editorFocus || editorTextFocus", }, { "key": "cmd+alt+enter", "command": "editor.action.replaceAll", "when": "editorFocus && findWidgetVisible", }, { "key": "ctrl+tab", "command": "workbench.action.nextEditor", "when": "editorTextFocus", }, { "key": "ctrl+shift+tab", "command": "workbench.action.previousEditor", "when": "editorTextFocus", }, { "key": "alt+r", "command": "toggleFindRegex", "when": "editorFocus && findWidgetVisible" }, { "key": "ctrl+0", "command": "workbench.action.zoomReset" }, { "key": "ctrl+numpad0", "command": "workbench.action.zoomReset" } ]