-- 마우스 보조 버튼을 누른 채 드래그할 때 스크롤로 변환하는 기능을 정의합니다. 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 로드 예외 처리 분기를 마칩니다. -- 공식 UnsplashRandom 대신 PicsumRandom Spoon을 로드해 다른 오픈소스 이미지 소스로 배경화면을 갱신합니다. local picsumRandomRefreshHotkeyModifiers = { "ctrl", "alt", "cmd" } -- 즉시 새 배경화면을 받는 단축키의 수정 키 조합을 정의합니다. local picsumRandomRefreshHotkeyKey = "w" -- 즉시 새 배경화면을 받는 단축키의 기본 키를 정의합니다. local picsumRandomRefreshHotkey = nil -- 실제로 등록된 즉시 새로고침 단축키 객체를 저장할 변수를 초기화합니다. local picsumRandomLoaded, picsumRandomError = pcall(hs.loadSpoon, "PicsumRandom") -- PicsumRandom Spoon 로드와 초기화를 예외 안전하게 시도합니다. if picsumRandomLoaded then -- PicsumRandom Spoon 로드에 성공한 경우를 처리합니다. local picsumRandomRefreshSystemConflict = hs.hotkey.systemAssigned(picsumRandomRefreshHotkeyModifiers, picsumRandomRefreshHotkeyKey) -- 시스템이 같은 단축키를 이미 점유하고 있는지 먼저 확인합니다. if picsumRandomRefreshSystemConflict then -- 시스템 단축키 충돌이 있는 경우를 처리합니다. hs.alert.show("ctrl-alt-cmd-w 단축키가 시스템에 이미 할당되어 등록하지 않았습니다") -- 단축키 충돌 사실을 사용자에게 화면으로 알립니다. hs.printf("Skipped binding PicsumRandom refresh hotkey due to system conflict: %s", hs.inspect(picsumRandomRefreshSystemConflict)) -- 충돌 세부 정보를 콘솔에 남겨 원인 파악을 돕습니다. else -- 시스템 단축키 충돌이 없는 경우를 처리합니다. picsumRandomRefreshHotkey = hs.hotkey.bind(picsumRandomRefreshHotkeyModifiers, picsumRandomRefreshHotkeyKey, function() -- 전역 단축키를 눌렀을 때 즉시 새 배경화면 갱신을 실행합니다. hs.alert.show("새 배경화면을 가져오는 중입니다") -- 단축키가 정상 인식됐음을 사용자에게 즉시 알려 줍니다. spoon.PicsumRandom:refresh() -- 현재 화면 크기에 맞는 새 랜덤 배경화면 다운로드를 바로 시작합니다. end) -- 즉시 새 배경화면 갱신 단축키 바인딩 정의를 마칩니다. end -- 시스템 단축키 충돌 여부에 따른 등록 분기를 마칩니다. hs.printf("PicsumRandom spoon loaded successfully") -- 콘솔에 성공 로그를 남겨 정상 로드를 확인할 수 있게 합니다. else -- PicsumRandom Spoon 로드에 실패한 경우 사용자에게 원인을 알립니다. hs.alert.show("PicsumRandom.spoon 설치를 확인해 주세요") -- 누락되었거나 손상된 Spoon 설치 상태를 사용자에게 안내합니다. hs.printf("Failed to load PicsumRandom spoon: %s", tostring(picsumRandomError)) -- 콘솔에 로드 실패 원인을 기록합니다. end -- PicsumRandom Spoon 로드 예외 처리 분기를 마칩니다.