Files
hammerspoon/init.lua
T
koreafood e970b7b04b feat: add cmd+b hotkey to auto-type stripped clipboard text
Add a new global Cmd+B hotkey that reads clipboard content, removes all hyphens,
and sequentially types the cleaned text into the currently focused application. Include
prerequisite checks for accessibility permissions and secure input state, error handling for
clipboard access and typing failures, and system hotkey conflict detection to avoid overriding
existing macOS shortcuts. Also add the project's git commit message guidelines in
.trae/rules/git-commit-message.md.
2026-06-26 08:56:18 +09:00

329 lines
29 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) -- 입력기 변경 콜백 등록을 마칩니다.
-- `cmd`+`b`로 클립보드 문자열에서 하이픈을 제거한 뒤 연속 자동 입력하는 기능 설정을 시작합니다.
local clipboardTypeHotkeyModifiers = { "cmd" } -- 자동 입력 기능에 사용할 수정 키 조합을 정의합니다.
local clipboardTypeHotkeyKey = "b" -- 자동 입력 기능에 사용할 기본 키를 정의합니다.
local clipboardTypeInterval = 0.12 -- 각 문자 입력 사이에 둘 짧은 지연 시간을 초 단위로 정의합니다.
local clipboardTypingInProgress = false -- 자동 입력이 이미 진행 중인지 추적해 중복 실행을 막습니다.
local clipboardTypeHotkey = nil -- 충돌 검사 후 실제로 바인딩된 단축키 객체를 저장합니다.
-- 문자열의 각 문자를 순서대로 담은 큐 테이블을 만드는 함수를 정의합니다.
local function buildCharacterQueue(text) -- 하이픈 제거 후 남은 모든 문자를 순차 전송하기 위한 준비 함수입니다.
local queue = {} -- 순서대로 전송할 문자 목록을 빈 테이블로 초기화합니다.
for _, codepoint in utf8.codes(text) do -- UTF-8 문자열을 코드포인트 단위로 안전하게 순회합니다.
queue[#queue + 1] = utf8.char(codepoint) -- 읽어온 문자를 그대로 큐 뒤에 추가합니다.
end -- 문자열 전체를 큐로 변환하는 반복을 마칩니다.
return queue -- 완성된 문자 큐를 호출한 곳에 반환합니다.
end -- 문자 큐 생성 함수 정의를 마칩니다.
-- 현재 환경에서 자동 키 입력을 수행할 수 있는지 선검사하는 함수를 정의합니다.
local function ensureClipboardTypingPrerequisites() -- 권한과 보안 입력 상태를 미리 확인해 실패를 줄입니다.
if not hs.accessibilityState() then -- 손쉬운 사용 권한이 없으면 키 이벤트 전송이 불가능하므로 먼저 처리합니다.
hs.accessibilityState(true) -- macOS 권한 허용 안내를 띄워 사용자가 바로 승인할 수 있게 돕습니다.
hs.alert.show("Hammerspoon 손쉬운 사용 권한이 필요합니다") -- 권한 필요 안내를 화면에 표시합니다.
return false -- 아직 자동 입력을 진행할 수 없으므로 실패를 반환합니다.
end -- 손쉬운 사용 권한 검사 분기 종료입니다.
if hs.eventtap.isSecureInputEnabled() then -- 보안 입력이 활성화된 앱에서는 키 이벤트 주입이 막힐 수 있으므로 점검합니다.
hs.alert.show("보안 입력이 켜져 있어 자동 입력을 실행할 수 없습니다") -- 현재 상태를 사용자에게 즉시 알립니다.
return false -- 안전하지 않은 상태에서는 자동 입력을 중단합니다.
end -- 보안 입력 상태 검사 분기 종료입니다.
return true -- 필요한 선행 조건이 모두 충족됐음을 반환합니다.
end -- 자동 입력 선행 조건 검사 함수 정의를 마칩니다.
-- 한 글자를 현재 포커스된 앱으로 전송하는 함수를 정의합니다.
local function postClipboardCharacter(character) -- 남아 있는 문자를 현재 포커스된 앱에 한 글자씩 안전하게 전송합니다.
hs.eventtap.keyStrokes(character, 0) -- 현재 포커스된 앱에 해당 문자를 한 글자씩 전달합니다.
end -- 단일 문자 전송 함수 정의를 마칩니다.
-- 문자 큐를 타이머 기반으로 순차 입력하는 함수를 정의합니다.
local function typeClipboardQueue(queue, index) -- 긴 입력 중에도 설정 전체가 멈추지 않도록 비동기식으로 처리합니다.
if not clipboardTypingInProgress then -- 외부 오류나 상태 변경으로 이미 종료됐다면 즉시 빠져나옵니다.
return -- 종료된 작업을 더 진행하지 않도록 여기서 함수를 마칩니다.
end -- 실행 상태 검사 분기 종료입니다.
if index > #queue then -- 마지막 문자까지 모두 전송했다면 정상 완료 처리를 진행합니다.
clipboardTypingInProgress = false -- 다음 실행을 위해 진행 중 상태를 해제합니다.
return -- 더 이상 전송할 문자가 없으므로 함수를 종료합니다.
end -- 입력 완료 여부 검사 분기 종료입니다.
local ok, errorMessage = pcall(postClipboardCharacter, queue[index]) -- 현재 문자를 안전하게 전송하고 예외를 가로챕니다.
if not ok then -- 문자 전송 중 런타임 오류가 발생한 경우를 처리합니다.
clipboardTypingInProgress = false -- 오류 발생 시에도 다음 실행을 위해 상태를 정상적으로 복구합니다.
hs.alert.show("자동 입력 중 오류가 발생했습니다") -- 사용자에게 실패 사실을 화면으로 알려줍니다.
hs.printf("Clipboard typing failed: %s", tostring(errorMessage)) -- 디버깅을 위해 콘솔에 구체적인 오류를 기록합니다.
return -- 오류 이후 추가 입력이 이어지지 않도록 즉시 종료합니다.
end -- 문자 전송 예외 처리 분기 종료입니다.
hs.timer.doAfter(clipboardTypeInterval, function() -- 다음 문자 입력을 짧은 지연 뒤 예약해 앱이 입력을 안정적으로 받게 합니다.
typeClipboardQueue(queue, index + 1) -- 다음 위치의 문자를 계속 순차적으로 전송합니다.
end) -- 다음 문자 예약 콜백 정의를 마칩니다.
end -- 문자 큐 순차 입력 함수 정의를 마칩니다.
-- 클립보드 내용을 읽어 하이픈 제거 후 연속 자동 입력 작업을 시작하는 함수를 정의합니다.
local function typeClipboardAsContinuousSequence() -- 실제 단축키가 눌렸을 때 호출될 메인 작업 함수입니다.
if clipboardTypingInProgress then -- 이전 자동 입력이 아직 끝나지 않았다면 중복 실행을 막습니다.
hs.alert.show("자동 입력이 이미 진행 중입니다") -- 재진입이 거부된 이유를 사용자에게 알려줍니다.
return -- 중복 실행은 허용하지 않고 함수를 종료합니다.
end -- 재진입 방지 분기 종료입니다.
if not ensureClipboardTypingPrerequisites() then -- 권한이나 보안 입력 상태가 적절한지 먼저 확인합니다.
return -- 선행 조건이 맞지 않으면 작업을 시작하지 않습니다.
end -- 선행 조건 검사 분기 종료입니다.
local clipboardReadOk, clipboardText = pcall(hs.pasteboard.getContents) -- 클립보드 문자열 읽기를 예외 안전하게 시도합니다.
if not clipboardReadOk then -- 클립보드 접근 자체에서 오류가 발생한 경우를 처리합니다.
hs.alert.show("클립보드 내용을 읽는 중 오류가 발생했습니다") -- 읽기 실패를 사용자에게 알립니다.
hs.printf("Clipboard read failed: %s", tostring(clipboardText)) -- 콘솔에 구체적인 오류 정보를 남깁니다.
return -- 읽기에 실패했으므로 자동 입력을 시작하지 않습니다.
end -- 클립보드 읽기 예외 처리 분기 종료입니다.
if type(clipboardText) ~= "string" or clipboardText == "" then -- 빈 문자열이거나 문자열이 아닌 경우는 유효한 입력 원본이 아닙니다.
hs.alert.show("클립보드에 입력할 텍스트가 없습니다") -- 사용자가 먼저 텍스트를 복사하도록 안내합니다.
return -- 유효한 내용이 없으므로 여기서 종료합니다.
end -- 클립보드 내용 유효성 검사 분기 종료입니다.
local transformedText = clipboardText:gsub("%-", "") -- 자동 포커스 이동을 활용할 수 있도록 모든 하이픈을 제거합니다.
local queue = buildCharacterQueue(transformedText) -- 하이픈이 제거된 문자열을 문자 단위 큐로 만들어 순차 입력 준비를 합니다.
if #queue == 0 then -- 하이픈 제거 후 결과가 실질적으로 비어 있으면 입력할 것이 없으므로 막습니다.
hs.alert.show("하이픈 제거 후 입력할 문자가 없습니다") -- 의미 없는 실행이 되지 않도록 사용자에게 알려줍니다.
return -- 입력 큐가 비어 있으므로 종료합니다.
end -- 하이픈 제거 결과 유효성 검사 분기 종료입니다.
clipboardTypingInProgress = true -- 실제 입력을 시작하기 전에 진행 중 상태를 활성화합니다.
local typingStartOk, typingStartError = pcall(typeClipboardQueue, queue, 1) -- 순차 입력 시작 자체도 예외 안전하게 감쌉니다.
if not typingStartOk then -- 시작 단계에서 예상치 못한 오류가 발생한 경우를 처리합니다.
clipboardTypingInProgress = false -- 시작 실패 시 진행 상태를 즉시 되돌립니다.
hs.alert.show("자동 입력 시작에 실패했습니다") -- 사용자에게 시작 실패를 알립니다.
hs.printf("Clipboard typing start failed: %s", tostring(typingStartError)) -- 콘솔에 실패 원인을 기록합니다.
end -- 자동 입력 시작 예외 처리 분기 종료입니다.
end -- 클립보드 연속 자동 입력 메인 함수 정의를 마칩니다.
-- 시스템 레벨에 이미 예약된 단축키와 충돌하는지 검사합니다.
local clipboardTypeSystemConflict = hs.hotkey.systemAssigned(clipboardTypeHotkeyModifiers, clipboardTypeHotkeyKey) -- macOS가 점유한 조합이면 바인딩을 피하기 위해 조회합니다.
if clipboardTypeSystemConflict then -- 시스템이 이미 이 단축키를 사용 중인 경우를 처리합니다.
hs.alert.show("cmd-b 단축키가 시스템에 이미 할당되어 있어 등록하지 않았습니다") -- 충돌 사실을 화면에 알립니다.
hs.printf("Skipped binding cmd-b due to system conflict: %s", hs.inspect(clipboardTypeSystemConflict)) -- 콘솔에 충돌 세부 정보를 남깁니다.
else -- 시스템 충돌이 없을 때만 안전하게 단축키를 등록합니다.
clipboardTypeHotkey = hs.hotkey.bind(clipboardTypeHotkeyModifiers, clipboardTypeHotkeyKey, function() -- 전역 단축키를 눌렀을 때 메인 작업을 실행합니다.
local hotkeyOk, hotkeyError = pcall(typeClipboardAsContinuousSequence) -- 단축키 콜백 전체를 예외 안전하게 감싸 강제 종료를 막습니다.
if not hotkeyOk then -- 콜백 바깥쪽에서 예상치 못한 오류가 터진 경우를 처리합니다.
clipboardTypingInProgress = false -- 오류 뒤 다음 실행이 가능하도록 상태를 정리합니다.
hs.alert.show("cmd-b 자동 입력 실행에 실패했습니다") -- 사용자에게 실행 실패를 알려줍니다.
hs.printf("cmd-b clipboard hotkey failed: %s", tostring(hotkeyError)) -- 콘솔에 실제 오류 메시지를 기록합니다.
end -- 단축키 콜백 예외 처리 분기 종료입니다.
end) -- 전역 단축키 바인딩 정의를 마칩니다.
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 로드 예외 처리 분기를 마칩니다.