Skip to content

Instantly share code, notes, and snippets.

@puryfury
Created January 27, 2026 03:40
Show Gist options
  • Select an option

  • Save puryfury/20ea70b862102236b52413d022e867c2 to your computer and use it in GitHub Desktop.

Select an option

Save puryfury/20ea70b862102236b52413d022e867c2 to your computer and use it in GitHub Desktop.
My hammerspoon script: hangul toggle by right option key, inverse mouse wheel with whitelist.
-- foundation_remapping.lua
-- https://github.com/hetima/hammerspoon-foundation_remapping
-- reference:
-- Technical Note TN2450 Remapping Keys in macOS 10.12 Sierra)
-- https://developer.apple.com/library/content/technotes/tn2450/
local FOUNDATION_REMAPPING_VERSION = '0.1.1'
local log = hs.logger.new('foundation_remapping', 'debug')
CFundationRemap = {
version = FOUNDATION_REMAPPING_VERSION,
}
-- Never use these values as it is.
-- Bitwise OR with 0x700000000 required. (or simply add 0x700000000)
CFundationRemap.hidkeys = {
[0x00] = 0x04, -- a
[0x0b] = 0x05,
[0x08] = 0x06,
[0x02] = 0x07,
[0x0e] = 0x08,
[0x03] = 0x09,
[0x05] = 0x0a,
[0x04] = 0x0b,
[0x22] = 0x0c,
[0x26] = 0x0d,
[0x28] = 0x0e,
[0x25] = 0x0f,
[0x2e] = 0x10,
[0x2d] = 0x11,
[0x1f] = 0x12,
[0x23] = 0x13,
[0x0c] = 0x14,
[0x0f] = 0x15,
[0x01] = 0x16,
[0x11] = 0x17,
[0x20] = 0x18,
[0x09] = 0x19,
[0x0d] = 0x1a,
[0x07] = 0x1b,
[0x10] = 0x1c,
[0x06] = 0x1d, -- z
[0x12] = 0x1e, -- 1
[0x13] = 0x1f,
[0x14] = 0x20,
[0x15] = 0x21,
[0x17] = 0x22,
[0x16] = 0x23,
[0x1a] = 0x24,
[0x1c] = 0x25,
[0x19] = 0x26,
[0x1d] = 0x27, -- 0
[0x24] = 0x28, -- Return
[0x35] = 0x29, -- esc
[0x33] = 0x2a, -- delete back space
[0x30] = 0x2b, -- tab
[0x31] = 0x2c, -- space
[0x1b] = 0x2d, -- - and _
[0x18] = 0x2e, -- = and +
[0x21] = 0x2f, -- [ and {
[0x1e] = 0x30, -- ] and }
[0x2a] = 0x31, -- \ and |
[0x2a] = 0x32, -- Non-US # and ~
[0x29] = 0x33, -- ; and :
[0x27] = 0x34, -- ' and "
[0x32] = 0x35, -- Grave Accent and Tilde E/J
[0x2b] = 0x36, -- , and "<"
[0x2f] = 0x37, -- . and ">"
[0x2c] = 0x38, -- / and ?
[0x39] = 0x39, -- Caps Lock
[0x7a] = 0x3a, -- F1
[0x78] = 0x3b,
[0x63] = 0x3c,
[0x76] = 0x3d,
[0x60] = 0x3e,
[0x61] = 0x3f,
[0x62] = 0x40,
[0x64] = 0x41,
[0x65] = 0x42,
[0x6d] = 0x43,
[0x67] = 0x44,
[0x6f] = 0x45, -- F12
[0x69] = 0x46, PrintScreen = 0x46, -- Print Screen
[0x6b] = 0x47, ScrollLock = 0x47, -- Scroll Lock
[0x71] = 0x48, Pause = 0x48, -- Pause
[0x72] = 0x49, Insert = 0x49,-- Insert conflict with help
[0x73] = 0x4a, -- Home
[0x74] = 0x4b, -- Page Up
[0x75] = 0x4c, -- Delete Forward
[0x77] = 0x4d, -- End
[0x79] = 0x4e, -- Page Down
[0x7c] = 0x4f, --Right arrow key, raw is 0x3c, virtual ADB is 0x7c
[0x7b] = 0x50, --Left arrow key, raw is 0x3b, virtual ADB is 0x7b
[0x7d] = 0x51, --Down arrow, raw is 0x3d, virtual is 0x7d
[0x7e] = 0x52, --Up arrow key, raw is 0x3e, virtual is 0x7e
[0x47] = 0x53, --Num Lock and Clear
[0x4b] = 0x54, -- Keypad /
[0x43] = 0x55, -- pad *
[0x4e] = 0x56, -- pad -
[0x45] = 0x57, -- pad +
[0x4c] = 0x58, -- pad Enter
[0x53] = 0x59, -- pad 1
[0x54] = 0x5a,
[0x55] = 0x5b,
[0x56] = 0x5c,
[0x57] = 0x5d,
[0x58] = 0x5e,
[0x59] = 0x5f,
[0x5b] = 0x60,
[0x5c] = 0x61,
[0x52] = 0x62, -- pad 0
[0x41] = 0x63, -- pad .
[0x0a] = 0x64, -- \ and | ISO only
[0x6e] = 0x65, Application=0x65,-- Application
[0x7f] = 0x66, --This is the power key, scan code in ADB is 7f 7f, not 7f ff
[0x51] = 0x67, -- pad =
-- [0x69] = 0x68, -- F13 on Andy keyboards conflict with PrintScreen
-- [0x6b] = 0x69, -- F14 on Andy keyboards conflict with ScrollLock
-- [0x71] = 0x6a, -- F15 on Andy keyboards conflict with Pause
[0x6a] = 0x6b, -- F16
[0x40] = 0x6c, -- F17
[0x4f] = 0x6d, -- F18
[0x50] = 0x6e, -- F19
[0x5a] = 0x6f, -- F20
f21=0x70, f22=0x71, f23=0x72, f24=0x73,
Execute=0x74,
-- [0x72] = 0x75, --help conflict with insert
Menu=0x76, Select=0x77, Stop=0x78, Again=0x79, Undo=0x7a, Cut=0x7b, Copy=0x7c, Paste=0x7d, Find=0x7e,
[0x4a] = 0x7f, -- Norsi Mute, or maybe 0x4a
[0x48] = 0x80, -- Norsi volume up, otherwise is 0x48 in ADB
[0x49] = 0x81, -- Norsi volume down
LockCapsLock=0x82, LockNumLocl=0x83, LockScrollLock=0x84,
[0x5f] = 0x85, -- pad , JIS only
-- padEqualSign=0x86
International1=0x87, [0x5e] = 0x87, JISUnderScore = 0x87,-- Ro (JIS) International1 _ ろ
International2=0x88, PCKana=0x88, -- PC Kana|Roma-ji
International3=0x89, [0x5d] = 0x89, -- Yen (JIS) ¥
International4=0x8a, XFER=0x8a, Henkan=0x8a, -- XFER 変換
International5=0x8b, NFER=0x8b, Muhenkan=0x8b,-- NFER 無変換
International6=0x8c, -- ,
International7=0x8d, -- DoubleByte/SingleByte
International8=0x8e, -- undef
International9=0x8f, -- undef
[0x68] = 0x90, -- Kana lang1
[0x66] = 0x91, -- Eisu lang2
lang3=0x92, --Hiragana?
lang4=0x93, --Katakana?
lang5=0x94, --Zenkaku/Hankaku?
lang6=0x95,
lang7=0x96,
lang8=0x97,
lang9=0x98,
[0x3b] = 0xe0, lctrl = 0xe0, lctl = 0xe0,--Left Control. raw is 0x36, virtual is 0x3b
[0x38] = 0xe1, lshift = 0xe1, --Left Shift
[0x3a] = 0xe2, lalt = 0xe2, lopt = 0xe2, --Left option/alt key
[0x37] = 0xe3, lcmd = 0xe3,--Left command key
[0x3e] = 0xe4, rctrl = 0xe4, rctl = 0xe4, --Right Control, use 0x3e virtual
[0x3c] = 0xe5, rshift = 0xe5, --Right Shift, use 0x3c virtual
[0x3d] = 0xe6, ralt = 0xe6, ropt = 0xe6, --Right Option, use 0x3d virtual
[0x36] = 0xe7, rcmd = 0xe7, --Right Command, use 0x36 virtual
-- 全角/半角キーはいくつか該当しそうなのがあるけれど、うちでは [0x32] = 0x35, -- Grave Accent and Tilde と判定される
}
for i, v in pairs(CFundationRemap.hidkeys) do
if type(v) == 'number' then
CFundationRemap.hidkeys[i] = v + 0x700000000
end
end
-- keyCode を数値に統一
local function realKeyCode(v)
if type(v) == 'string' then
v = hs.keycodes.map[v]
end
if type(v) == 'number' then
return v
end
return nil
end
-- keyCode を hidutil で使える値に変換
local function hidKeyCode(keyCode)
local hidCode = nil
if (type(keyCode) == 'number') and (keyCode > 0x700000000) then
return keyCode
end
--hidkeys[string] があるかどうか
if type(keyCode) == 'string' then
hidCode = CFundationRemap.hidkeys[keyCode]
if hidCode ~= nil then
return hidCode
end
end
keyCode = realKeyCode(keyCode)
if keyCode ~= nil then
--数値keyCodeで探す
return CFundationRemap.hidkeys[keyCode]
end
return nil
end
local CFundationRemapImpl = {
remap = function(self, fromKey, toKey)
fromKey = hidKeyCode(fromKey)
toKey = hidKeyCode(toKey)
if fromKey and toKey then
table.insert(self._remaps, {from=fromKey, to=toKey})
end
return self
end,
nullfy = function(self, fromKey)
fromKey = hidKeyCode(fromKey)
if fromKey then
table.insert(self._remaps, {from=fromKey, to=0})
end
return self
end,
-- --filter '{"ProductID":...,"VendorID":...}'
_filterArgument = function(self)
local filter = ''
if self.productID then
filter = '"ProductID":' .. self.productID .. ','
end
if self.vendorID then
filter = filter .. '"VendorID":' .. self.vendorID .. ','
end
local optionName = '--filter'
if os.execute("hidutil property --help | grep -e '--matching'") then
optionName = '--matching'
end
if #filter > 0 then
return ' ' .. optionName .. ' \'{' .. filter .. '}\''
end
return ''
end,
-- /usr/bin/hidutil property --filter '{"ProductID":...,"VendorID":...}' --set '{"UserKeyMapping":[{...},...]}'
command = function(self)
if #self._remaps == 0 then
return nil
end
local filter = self:_filterArgument()
local cmd = '/usr/bin/hidutil property' .. filter .. ' --set \'{"UserKeyMapping":['
for i, v in ipairs(self._remaps) do
if type(v) == 'table' then
cmd = cmd .. '{"HIDKeyboardModifierMappingSrc":' .. v.from .. ',"HIDKeyboardModifierMappingDst":' .. v.to .. '},'
end
end
cmd = cmd .. ']}\''
return cmd
end,
-- /usr/bin/hidutil property --filter '{"ProductID":...,"VendorID":...}' --set '{"UserKeyMapping":[]}'
resetCommand = function(self)
local filter = self:_filterArgument()
local cmd = '/usr/bin/hidutil property' .. filter .. ' --set \'{"UserKeyMapping":[]}\''
return cmd
end,
register = function(self)
local cmd = self:command()
if cmd then
if os.execute(cmd) ~= true then
log.d('error occured while register()')
log.d('command:' .. cmd)
end
end
return self
end,
unregister = function(self)
local cmd = self:resetCommand()
if cmd then
if os.execute(cmd) ~= true then
log.d('error occured while unregister()')
log.d('command:' .. cmd)
end
end
return self
end,
}
CFundationRemap.new = function(opt)
local _self = {
_remaps = {},
vendorID = nil,
productID = nil,
}
setmetatable(_self, {__index = CFundationRemapImpl})
if type(opt) == 'table' then
if type(opt.vendorID) == 'number' then
_self.vendorID = opt.vendorID
end
if type(opt.productID) == 'number' then
_self.productID = opt.productID
end
end
return _self
end
return CFundationRemap
local FRemap = require('foundation_remapping')
local remapper = FRemap.new()
remapper:remap('ralt', 'f18')
-- 제외할 앱 번들 ID 배열
local excludedApps = {
'com.microsoft.rdc.macos',
-- 필요하면 더 추가
}
-- 현재 앱이 제외 목록에 있는지 확인
local function isExcludedApp(bundleID)
for _, id in ipairs(excludedApps) do
if id == bundleID then
return true
end
end
return false
end
-- 리맵 상태 추적
local isRemapActive = false
-- 앱 전환 시 리맵 적용/해제
local function updateRemap()
local app = hs.application.frontmostApplication()
local bundleID = app:bundleID()
if isExcludedApp(bundleID) then
-- 제외 앱이면 리맵 해제
if isRemapActive then
remapper:unregister()
isRemapActive = false
end
else
-- 일반 앱이면 리맵 적용
if not isRemapActive then
remapper:register()
isRemapActive = true
end
end
end
-- 앱 전환 감지
appWatcher = hs.application.watcher.new(function(name, event, app)
if event == hs.application.watcher.activated then
updateRemap()
end
end)
appWatcher:start()
-- 초기 실행 시 현재 앱 기준으로 적용
updateRemap()
-- 마우스 화이트리스트 (hs.mouse.names()에서 출력되는 이름 기준)
local mouseWhitelist = {
["Logitech::Logitech Pebble"] = true,
-- ["Logitech::MX Master 3S"] = true, -- 추가하고 싶다면 이처럼 작성
}
-- 화이트리스트 모드 활성화 여부
local useWhitelist = true
-- 마우스가 화이트리스트에 있는지 확인 (이름 기반)
local function isMouseWhitelisted()
local connectedNames = hs.mouse.names()
for _, name in ipairs(connectedNames) do
if mouseWhitelist[name] then
return true
end
end
return false
end
-- 스크롤 반전 이벤트탭
reverse_mouse_scroll = hs.eventtap.new({hs.eventtap.event.types.scrollWheel}, function(event)
-- 1. 트랙패드 감지 (연속 스크롤인 경우 통과)
local isTrackpad = event:getProperty(hs.eventtap.event.properties.scrollWheelEventIsContinuous)
if isTrackpad == 1 then
return false
end
-- 2. 화이트리스트 체크 (hs.mouse.names() 기반이므로 비용이 매우 저렴함)
if useWhitelist and not isMouseWhitelisted() then
return false
end
-- 3. Y축 스크롤 반전 적용
event:setProperty(hs.eventtap.event.properties.scrollWheelEventDeltaAxis1,
-event:getProperty(hs.eventtap.event.properties.scrollWheelEventDeltaAxis1))
return false
end):start()
-- [편의용 함수]
-- 현재 연결된 마우스 목록과 화이트리스트 여부를 콘솔에 출력
function showMice()
local names = hs.mouse.names()
print("\n=== 감지된 마우스 목록 ===")
for i, name in ipairs(names) do
local status = mouseWhitelist[name] and "[✓ 화이트리스트]" or "[ 미등록 장치]"
print(string.format("%d. %s : %s", i, status, name))
end
print("화이트리스트 모드: " .. (useWhitelist and "ENABLED" or "DISABLED"))
end
-- 화이트리스트 모드 토글 (알림창 포함)
function toggleWhitelist()
useWhitelist = not useWhitelist
hs.alert.show("마우스 화이트리스트: " .. (useWhitelist and "ON" or "OFF"))
showMice()
end
-- 초기 실행 시 목록 한 번 출력
showMice()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment