Files
hammerspoon/init.lua
T
2026-06-24 16:55:25 +09:00

213 lines
18 KiB
Lua

-- 마우스 보조 버튼을 누른 채 드래그할 때 스크롤로 변환하는 기능을 정의합니다.
local scrollMouseButton = 2 -- 스크롤 전환에 사용할 보조 마우스 버튼 번호를 지정합니다.
local deferred = false -- 클릭으로 처리할지 스크롤로 처리할지 보류 상태를 저장합니다.
-- 보조 마우스 버튼을 누르는 순간을 감지하는 이벤트 탭을 만듭니다.
overrideOtherMouseDown = hs.eventtap.new({ hs.eventtap.event.types.otherMouseDown }, function(e) -- 보조 버튼 눌림 이벤트를 가로챕니다.
local pressedMouseButton = e:getProperty(hs.eventtap.event.properties["mouseEventButtonNumber"]) -- 실제로 눌린 보조 버튼 번호를 읽습니다.
if scrollMouseButton == pressedMouseButton then -- 설정한 스크롤 전환 버튼이 눌린 경우에만 처리합니다.
deferred = true -- 아직 드래그 여부를 모르므로 클릭 후보 상태로 표시합니다.
return true -- 기본 동작을 잠시 막고 이후 로직에서 결정합니다.
end -- 설정 버튼이 아닌 경우 분기 종료입니다.
end) -- 보조 버튼 눌림 이벤트 탭 정의를 마칩니다.
-- 보조 마우스 버튼을 놓는 순간 클릭으로 되돌릴지 결정하는 이벤트 탭을 만듭니다.
overrideOtherMouseUp = hs.eventtap.new({ hs.eventtap.event.types.otherMouseUp }, function(e) -- 보조 버튼 떼기 이벤트를 가로챕니다.
local pressedMouseButton = e:getProperty(hs.eventtap.event.properties["mouseEventButtonNumber"]) -- 떼어진 보조 버튼 번호를 읽습니다.
if scrollMouseButton == pressedMouseButton then -- 설정한 스크롤 전환 버튼인 경우에만 처리합니다.
if deferred then -- 드래그가 없어서 아직 클릭 후보 상태라면 클릭으로 복원합니다.
overrideOtherMouseDown:stop() -- 재귀 입력을 막기 위해 눌림 이벤트 탭을 잠시 멈춥니다.
overrideOtherMouseUp:stop() -- 재귀 입력을 막기 위해 떼기 이벤트 탭을 잠시 멈춥니다.
hs.eventtap.otherClick(e:location(), pressedMouseButton) -- 원래 의도한 보조 버튼 클릭을 다시 발생시킵니다.
overrideOtherMouseDown:start() -- 눌림 이벤트 탭을 다시 시작합니다.
overrideOtherMouseUp:start() -- 떼기 이벤트 탭을 다시 시작합니다.
return true -- 원본 이벤트는 여기서 처리했으므로 시스템 기본 동작을 막습니다.
end -- 클릭 후보 상태 처리 분기 종료입니다.
return false -- 이미 드래그로 처리됐다면 기본 해제를 허용합니다.
end -- 설정 버튼인 경우 분기 종료입니다.
return false -- 다른 버튼은 기본 동작을 그대로 유지합니다.
end) -- 보조 버튼 떼기 이벤트 탭 정의를 마칩니다.
local oldmousepos = {} -- 스크롤 처리 중 유지할 마우스 절대 위치를 저장합니다.
local scrollmult = -4 -- 드래그 이동량을 스크롤 양으로 바꿀 배수를 지정합니다.
-- 보조 버튼 드래그를 스크롤 이벤트로 바꾸는 이벤트 탭을 만듭니다.
dragOtherToScroll = hs.eventtap.new({ hs.eventtap.event.types.otherMouseDragged }, function(e) -- 보조 버튼 드래그 이벤트를 가로챕니다.
local pressedMouseButton = e:getProperty(hs.eventtap.event.properties["mouseEventButtonNumber"]) -- 드래그 중인 보조 버튼 번호를 읽습니다.
if scrollMouseButton == pressedMouseButton then -- 설정한 스크롤 전환 버튼으로 드래그 중일 때만 처리합니다.
deferred = false -- 드래그가 발생했으므로 더 이상 클릭 후보가 아니라고 표시합니다.
oldmousepos = hs.mouse.getAbsolutePosition() -- 커서를 원래 위치에 유지하기 위해 현재 위치를 저장합니다.
local dx = e:getProperty(hs.eventtap.event.properties["mouseEventDeltaX"]) -- 가로 이동량을 읽습니다.
local dy = e:getProperty(hs.eventtap.event.properties["mouseEventDeltaY"]) -- 세로 이동량을 읽습니다.
local scroll = hs.eventtap.event.newScrollEvent({ -dx * scrollmult, dy * scrollmult }, {}, "pixel") -- 이동량을 픽셀 스크롤 이벤트로 변환합니다.
hs.mouse.setAbsolutePosition(oldmousepos) -- 커서 위치가 움직이지 않도록 원래 좌표로 되돌립니다.
return true, { scroll } -- 원본 드래그를 소비하고 새 스크롤 이벤트를 대신 보냅니다.
else -- 설정한 버튼이 아닌 드래그는 건드리지 않습니다.
return false, {} -- 다른 버튼 드래그는 기본 동작을 유지합니다.
end -- 버튼 종류에 따른 분기 종료입니다.
end) -- 보조 버튼 드래그 스크롤 이벤트 탭 정의를 마칩니다.
-- 일반 마우스 휠 방향을 뒤집는 이벤트 탭을 만듭니다.
reverseScrollWheel = hs.eventtap.new({ hs.eventtap.event.types.scrollWheel }, function(e) -- 스크롤 휠 이벤트를 가로챕니다.
local isContinuous = e:getProperty(hs.eventtap.event.properties.scrollWheelEventIsContinuous) -- 트랙패드 같은 연속 스크롤인지 확인합니다.
if isContinuous == 1 then -- 연속 스크롤 장치는 뒤집지 않고 그대로 둡니다.
return false -- 트랙패드 기본 동작을 유지합니다.
end -- 연속 스크롤 장치 분기 종료입니다.
local vertical = e:getProperty(hs.eventtap.event.properties.scrollWheelEventDeltaAxis1) or 0 -- 세로 휠 이동량을 읽습니다.
local fixed = hs.eventtap.event.newScrollEvent({ 0, -vertical }, {}, "line") -- 세로 휠 방향을 반전한 새 라인 스크롤 이벤트를 만듭니다.
reverseScrollWheel:stop() -- 재귀로 다시 잡히지 않도록 이벤트 탭을 잠시 멈춥니다.
fixed:post() -- 반전된 스크롤 이벤트를 시스템에 전송합니다.
reverseScrollWheel:start() -- 스크롤 감지를 계속하기 위해 이벤트 탭을 다시 시작합니다.
return true -- 원본 휠 이벤트는 직접 처리했으므로 소비합니다.
end) -- 마우스 휠 방향 반전 이벤트 탭 정의를 마칩니다.
-- 오른쪽 커맨드와 오른쪽 옵션 키로 입력기를 전환하는 설정입니다.
local englishSource = "com.apple.keylayout.ABC" -- 영문 입력기 소스 ID를 지정합니다.
local koreanSource = "com.apple.inputmethod.Korean.2SetKorean" -- 한글 입력기 소스 ID를 지정합니다.
local rightCommandKeyCode = hs.keycodes.map.rightcmd -- 오른쪽 커맨드 키 코드를 가져옵니다.
local rightOptionKeyCode = hs.keycodes.map.rightalt -- 오른쪽 옵션 키 코드를 가져옵니다.
local inputSourceHotkey = nil -- 현재 입력기 전환 후보 키를 저장합니다.
local inputSourceHotkeyUsed = false -- 후보 키가 다른 조합에 사용됐는지 추적합니다.
-- 현재 입력기가 다를 때만 원하는 입력기로 전환하는 함수를 정의합니다.
local function setInputSource(sourceID) -- 입력기 변경 로직을 공통 함수로 묶습니다.
if hs.keycodes.currentSourceID() ~= sourceID then -- 이미 원하는 입력기가 아닐 때만 전환합니다.
hs.keycodes.currentSourceID(sourceID) -- macOS 입력기를 지정한 소스로 변경합니다.
end -- 입력기 전환 필요 여부 분기 종료입니다.
end -- 입력기 변경 함수 정의를 마칩니다.
local appInputSourceRules = { -- 앱별로 자동 적용할 입력기 규칙을 번들 ID 기준으로 정의합니다.
["com.brave.Browser"] = englishSource, -- Brave Browser가 활성화되면 영문 입력기로 맞춥니다.
["com.apple.Terminal"] = englishSource, -- macOS 기본 Terminal이 활성화되면 영문 입력기로 맞춥니다.
["com.googlecode.iterm2"] = englishSource, -- iTerm2가 활성화되면 영문 입력기로 맞춥니다.
["com.microsoft.VSCode"] = englishSource, -- VS Code가 활성화되면 영문 입력기로 맞춥니다.
} -- 앱별 입력기 규칙 테이블 정의를 마칩니다.
local function applyAppInputSource(appObject) -- 현재 활성 앱에 맞는 입력기 규칙을 적용하는 함수를 정의합니다.
if appObject == nil then -- 활성 앱 정보를 받지 못한 경우를 먼저 처리합니다.
return -- 앱 정보가 없으면 적용할 규칙을 찾을 수 없으므로 종료합니다.
end -- 앱 정보 유효성 검사 분기 종료입니다.
local bundleID = appObject:bundleID() -- 활성 앱의 번들 ID를 읽어 규칙 조회에 사용합니다.
local targetSource = appInputSourceRules[bundleID] -- 번들 ID에 해당하는 목표 입력기를 조회합니다.
if targetSource ~= nil then -- 등록된 규칙이 있는 앱인 경우에만 입력기를 바꿉니다.
setInputSource(targetSource) -- 현재 활성 앱에 맞는 입력기로 전환합니다.
end -- 앱별 입력기 규칙 적용 분기 종료입니다.
end -- 앱별 입력기 적용 함수 정의를 마칩니다.
appInputSourceWatcher = hs.application.watcher.new(function(appName, eventType, appObject) -- 앱 활성화 시점을 감시하는 워처를 생성합니다.
if eventType ~= hs.application.watcher.activated then -- 앱이 전면으로 활성화된 경우가 아니면 무시합니다.
return -- 활성화 이벤트가 아니면 입력기를 바꾸지 않고 종료합니다.
end -- 앱 활성화 이벤트 필터 분기 종료입니다.
applyAppInputSource(appObject) -- 활성화된 앱의 규칙이 있으면 해당 입력기를 적용합니다.
end) -- 앱별 입력기 자동 전환 워처 정의를 마칩니다.
-- 오른쪽 수정 키 단독 입력을 감지해서 입력기를 바꾸는 이벤트 탭을 만듭니다.
inputSourceWatcher = hs.eventtap.new({ -- 플래그 변경과 키 입력을 함께 감시합니다.
hs.eventtap.event.types.flagsChanged, -- 수정 키 눌림과 떼기 변화를 감지합니다.
hs.eventtap.event.types.keyDown -- 일반 키 입력으로 조합 사용 여부를 판별합니다.
}, function(e) -- 입력기 전환 판단 콜백을 정의합니다.
local eventType = e:getType() -- 현재 이벤트 종류를 읽습니다.
local keyCode = e:getKeyCode() -- 현재 이벤트의 키 코드를 읽습니다.
if eventType == hs.eventtap.event.types.keyDown then -- 일반 키가 눌린 상황이면 단독 입력이 아니라고 봅니다.
if inputSourceHotkey ~= nil then -- 수정 키가 후보 상태라면 조합에 사용된 것으로 표시합니다.
inputSourceHotkeyUsed = true -- 단독 입력 전환이 일어나지 않도록 사용 상태를 기록합니다.
end -- 후보 상태인 경우 분기 종료입니다.
return false -- 일반 키 입력은 그대로 시스템에 전달합니다.
end -- 일반 키 이벤트 분기 종료입니다.
local flags = e:getFlags() -- 현재 활성화된 수정 키 상태를 읽습니다.
if keyCode == rightCommandKeyCode then -- 오른쪽 커맨드 키 변화인 경우를 처리합니다.
if flags.cmd then -- 오른쪽 커맨드를 누른 순간이면 후보 상태를 시작합니다.
inputSourceHotkey = "rightcmd" -- 현재 후보 키를 오른쪽 커맨드로 저장합니다.
inputSourceHotkeyUsed = false -- 아직 다른 키와 조합되지 않았다고 초기화합니다.
else -- 오른쪽 커맨드를 뗀 순간이면 단독 입력 여부를 판정합니다.
local shouldSwitch = inputSourceHotkey == "rightcmd" and not inputSourceHotkeyUsed -- 단독 입력이었다면 한글 전환 대상으로 판단합니다.
inputSourceHotkey = nil -- 후보 키 상태를 초기화합니다.
inputSourceHotkeyUsed = false -- 사용 여부 상태도 함께 초기화합니다.
if shouldSwitch then -- 단독 입력으로 판정된 경우에만 입력기를 바꿉니다.
setInputSource(koreanSource) -- 오른쪽 커맨드 단독 입력 시 한글 입력기로 전환합니다.
return true -- 이벤트를 처리했으므로 기본 동작을 막습니다.
end -- 오른쪽 커맨드 단독 입력 처리 분기 종료입니다.
end -- 오른쪽 커맨드 눌림과 떼기 분기 종료입니다.
elseif keyCode == rightOptionKeyCode then -- 오른쪽 옵션 키 변화인 경우를 처리합니다.
if flags.alt then -- 오른쪽 옵션을 누른 순간이면 후보 상태를 시작합니다.
inputSourceHotkey = "rightalt" -- 현재 후보 키를 오른쪽 옵션으로 저장합니다.
inputSourceHotkeyUsed = false -- 아직 다른 키와 조합되지 않았다고 초기화합니다.
else -- 오른쪽 옵션을 뗀 순간이면 단독 입력 여부를 판정합니다.
local shouldSwitch = inputSourceHotkey == "rightalt" and not inputSourceHotkeyUsed -- 단독 입력이었다면 영문 전환 대상으로 판단합니다.
inputSourceHotkey = nil -- 후보 키 상태를 초기화합니다.
inputSourceHotkeyUsed = false -- 사용 여부 상태도 함께 초기화합니다.
if shouldSwitch then -- 단독 입력으로 판정된 경우에만 입력기를 바꿉니다.
setInputSource(englishSource) -- 오른쪽 옵션 단독 입력 시 영문 입력기로 전환합니다.
return true -- 이벤트를 처리했으므로 기본 동작을 막습니다.
end -- 오른쪽 옵션 단독 입력 처리 분기 종료입니다.
end -- 오른쪽 옵션 눌림과 떼기 분기 종료입니다.
elseif inputSourceHotkey ~= nil then -- 다른 수정 키 변화가 끼어들면 단독 입력 후보를 무효화합니다.
inputSourceHotkeyUsed = true -- 단독 입력이 아닌 조합으로 간주합니다.
end -- 입력기 전환 후보 상태 처리 분기 종료입니다.
return false -- 처리하지 않은 이벤트는 시스템 기본 동작을 유지합니다.
end) -- 입력기 전환 이벤트 탭 정의를 마칩니다.
-- 현재 입력기가 한글인지 확인하는 함수를 정의합니다.
local function isKoreanInputActive() -- 한글 입력 상태 판별을 공통 함수로 묶습니다.
return hs.keycodes.currentSourceID() == koreanSource -- 현재 입력기 소스 ID가 한글 입력기와 같은지 반환합니다.
end -- 한글 입력 상태 판별 함수 정의를 마칩니다.
-- 입력기 전환 시 화면 중앙에 짧게 표시할 글자와 스타일을 정의합니다.
local inputSourceHudDuration = 0.35 -- 입력기 전환 표시가 화면에 머무를 시간을 초 단위로 지정합니다.
local inputSourceHudStyle = { -- 입력기 전환 표시의 시각 스타일을 한곳에 모아 둡니다.
fillColor = { white = 0.05, alpha = 0.78 }, -- 배경을 어둡고 반투명하게 설정합니다.
strokeColor = { white = 1, alpha = 0 }, -- 테두리는 보이지 않도록 투명하게 설정합니다.
strokeWidth = 0, -- 테두리 두께를 없애 깔끔하게 표시합니다.
radius = 14, -- 둥근 모서리 반경을 지정합니다.
textColor = { white = 1, alpha = 1 }, -- 글자색은 흰색으로 지정합니다.
textSize = 34, -- 중앙 표시가 잘 보이도록 글자 크기를 키웁니다.
} -- 입력기 전환 표시 스타일 정의를 마칩니다.
-- 현재 입력기 상태에 맞는 짧은 중앙 표시를 띄우는 함수를 정의합니다.
local function showInputSourceHud() -- 입력기 전환 알림 표시를 공통 함수로 묶습니다.
hs.alert.closeAll() -- 이전에 떠 있던 입력기 전환 알림을 먼저 닫아 하나만 보이게 만듭니다.
if isKoreanInputActive() then -- 현재 입력기가 한글인 경우를 처리합니다.
hs.alert.show("한", inputSourceHudStyle, hs.screen.mainScreen(), inputSourceHudDuration) -- 화면 중앙에 짧게 한글 표시를 띄웁니다.
else -- 현재 입력기가 영문인 경우를 처리합니다.
hs.alert.show("A", inputSourceHudStyle, hs.screen.mainScreen(), inputSourceHudDuration) -- 화면 중앙에 짧게 영문 표시를 띄웁니다.
end -- 입력기 종류에 따른 표시 분기를 마칩니다.
end -- 입력기 전환 알림 표시 함수 정의를 마칩니다.
-- 입력기 변경 시 화면 중앙 표시가 자동으로 뜨도록 콜백을 등록합니다.
hs.keycodes.inputSourceChanged(function() -- 입력기 전환이 일어날 때마다 호출될 함수를 등록합니다.
showInputSourceHud() -- 한글과 영문 전환 결과에 맞는 중앙 표시를 즉시 띄웁니다.
end) -- 입력기 변경 콜백 등록을 마칩니다.
overrideOtherMouseDown:start() -- 보조 버튼 눌림 감시를 시작합니다.
overrideOtherMouseUp:start() -- 보조 버튼 떼기 감시를 시작합니다.
dragOtherToScroll:start() -- 보조 버튼 드래그 스크롤 감시를 시작합니다.
reverseScrollWheel:start() -- 마우스 휠 방향 반전 감시를 시작합니다.
inputSourceWatcher:start() -- 입력기 전환 감시를 시작합니다.
appInputSourceWatcher:start() -- 앱 전환 시 입력기 자동 적용 감시를 시작합니다.
hs.timer.doAfter(0, function() -- 설정 리로드 직후 현재 전면 앱에도 규칙을 한 번 적용합니다.
applyAppInputSource(hs.application.frontmostApplication()) -- 이미 열려 있는 현재 앱 상태와 입력기를 즉시 맞춥니다.
end) -- 초기 전면 앱 입력기 동기화 예약을 마칩니다.
-- 공식 Caffeine Spoon을 로드해 메뉴바와 단축키 기반 절전 방지 기능을 활성화합니다.
local caffeineLoaded, caffeineError = pcall(hs.loadSpoon, "Caffeine") -- Caffeine Spoon 로드를 안전하게 시도합니다.
if caffeineLoaded then -- Caffeine Spoon 로드에 성공한 경우에만 기능을 활성화합니다.
spoon.Caffeine:start() -- 메뉴바 Caffeine 아이콘과 내부 상태 관리를 시작합니다.
spoon.Caffeine:bindHotkeys({ -- Caffeine 토글 단축키를 등록합니다.
toggle = { { "ctrl", "alt", "cmd" }, "c" } -- `ctrl`+`alt`+`cmd`+`c` 조합으로 절전 방지를 토글합니다.
}) -- Caffeine 단축키 등록 블록을 닫습니다.
else -- Caffeine Spoon 로드에 실패한 경우 사용자에게 원인을 알립니다.
hs.alert.show("Caffeine.spoon 설치가 필요합니다") -- 누락된 Spoon 설치 필요 안내를 표시합니다.
hs.printf("Failed to load Caffeine spoon: %s", tostring(caffeineError)) -- 콘솔에 로드 실패 원인을 기록합니다.
end -- Caffeine Spoon 로드 예외 처리 분기를 마칩니다.