Skip to content

Instantly share code, notes, and snippets.

@derek-shnosh
Last active March 12, 2026 23:08
Show Gist options
  • Select an option

  • Save derek-shnosh/13c82872da665c32d181340c9a45e7ad to your computer and use it in GitHub Desktop.

Select an option

Save derek-shnosh/13c82872da665c32d181340c9a45e7ad to your computer and use it in GitHub Desktop.
Windows Keybinding config for MacOS 26 using Hammerspoon
---@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.")
@derek-shnosh
Copy link
Author

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"
    }
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment