"녹화는 되는데 이름은 못 붙이는 조합이었어."
내 것도 아닌 키를 듣는 방법 셋
다른 앱이 맨 앞에 있을 때도 단축키에 반응해야 하는 유틸리티가 쓸 수 있는 도구는 세 가지고, 저마다 치르는 대가가 달라.
- Carbon
RegisterEventHotKey는 조합(키 하나에 수정 키들)을 시스템 전체에 등록하고 앱에 단축키 이벤트를 보내줘. 손쉬운 사용 권한이 필요 없고, 다른 키 입력은 못 봐. 가족 텍스트 변환기, 받아쓰기 도구, 런처들이 아직 이걸 써. API는 낡았지만 이 일을 대신할 더 새로운 API가 없어. NSEvent.addGlobalMonitorForEvents는 다른 앱으로 가는 이벤트를 지켜봐. 키 관련 이벤트는 앱이 손쉬운 사용 권한을 받았을 때만 오고, 모니터는 보기만 하지 가로채진 못해.CGEventTap은 이벤트를 듣기만 할 수도 있고, 능동 필터로서 바꾸거나 삼킬 수도 있어. 듣기만 하려면 입력 모니터링, 능동 탭은 손쉬운 사용 권한이 필요하고, 엉뚱한 이벤트를 삼키는 탭은 그 키를 Mac의 모든 앱에서 먹어버려.
등록은 조합마다 실패할 수 있어
RegisterEventHotKey는 이미 등록된 조합이면 eventHotKeyExistsErr(-9878)를 돌려줘. 한 프로세스에서 ⌘⌥K를 두 번 등록해서 재봤어. 첫 번째는 성공, 두 번째는 -9878. 텍스트 변환기는 켜진 매크로를 하나씩 따로 등록하고 실패를 바인딩마다 설정 창에 돌려줘. 그래서 하나가 겹쳐도 나머지는 살아 있고, 앱 안에서 서로 겹친 조합은 Carbon까지 가기 전에 이름을 붙여 보고해.
펑션 키는 범위가 아니야
가상 키 코드는 키보드 위의 물리적 위치지 순서가 아니야. kVK_F1은 0x7A고 kVK_F12는 0x6F야. 그래서 UInt16(kVK_F1)...UInt16(kVK_F12)는 아래 경계가 위 경계보다 커. 이걸로 만든 단축키 녹화기는 아무 키나 누르는 순간 "Range requires lowerBound <= upperBound"로 죽었어. 상수를 집합으로 하나하나 적어. 녹화기는 NSEvent에 묶여 있어서 테스트가 없었어. 그래서 그 검사는 셀프 테스트가 붙은 순수 도우미 함수로 옮겼어.
앱이 이름 못 붙이는 키는 받지도 마
앱 세 개가 같은 키 코드와 기호 표를 각자 복사해서 들고 있었어. 한 녹화기는 Escape, Caps Lock, Fn만 빼고 아무 키나 받았는데, 표엔 Home, End, Page Up과 Down, F13부터 F20, 키패드가 없었어. 그래서 ⌘⌥Home은 녹화되고, 저장되고, 등록되고, 단축키도 잘 됐는데 설정엔 "⌘⌥Key 115"로 보였어. 이제 공유 키트 타입은 "이 키에 이름을 붙일 수 있나"를 "이 앱이 이 키를 받나"와 따로 대답해. 키 값은 완전한 표를 거쳐야만 만들 수 있으니까 이름 없는 표시는 생길 수가 없어. 앱별 제외 목록은 그 위에 각자 얹어.
수정 키 하나로 누르고 말하기
오른쪽 ⌘나 Fn을 누르고 있는 동안 받아쓰기하는 것처럼, 수정 키만으로 된 조합은 Carbon으로 표현할 수 없어. 받아쓰기 도구는 이미 받은 손쉬운 사용 권한으로 flagsChanged를 지켜봐. 다른 앱용으로 전역 모니터, 자기 창용으로 로컬 모니터를 둬(전역 모니터는 자기 앱으로 오는 이벤트를 절대 안 봐). 눌렸는지 떼졌는지는 CGEventSource.keyState로 정해. 반대쪽 ⌘ 키가 아직 눌려 있으면 수정 키 플래그가 계속 켜져 있거든. Fn은 지구본 키도 겸하니까, 사용자는 지구본 키 동작을 아무것도 안 함으로 바꿔둬.