Kolibri는 작은 Mac 유틸리티입니다. fn 키와 control 키를 누른 채 마우스를 움직이면 포인터 아래의 창이 따라옵니다. option 키를 더하면 가장 가까운 모서리에서 크기를 조절합니다. Linux 데스크톱에 오래전부터 있던 Alt 드래그인데, 저는 Mac에서 매일 이 기능이 아쉬웠습니다.
이런 유틸리티는 늘 손쉬운 사용 API(Accessibility)로 만들어 왔습니다. 시스템에 포인터 아래에 있는 창을 물어본 다음, 마우스가 움직일 때마다 그 창의 위치를 바꿉니다. 잘 작동합니다. 하지만 Mac App Store에는 올릴 수 없습니다. 손쉬운 사용 API는 샌드박스 안에서 작동하지 않는데, App Store 앱은 모두 샌드박스를 적용해야 하기 때문입니다. 그래서 Moom 4는 Many Tricks의 자체 사이트에서만 판매됩니다. 스토어에 남아 있는 Moom은 지금 Moom Classic이라고 불리는 Moom 3입니다. 샌드박스 규칙이 생기기 전에 들어갔고, 새 기능을 추가하지 않는 한 스토어에 남을 수 있습니다.
그래도 저는 Kolibri를 스토어에 올리고 싶었습니다. 대부분의 사람이 Mac 앱을 스토어에서 찾고, 저는 결제 시스템을 직접 만들고 싶지 않았습니다. 그 과정은 이랬습니다.
샌드박스 앱이 할 수 있는 일
샌드박스 앱은 다른 앱의 창을 건드릴 수 없습니다. 하지만 알맞은 entitlement와 사용자의 허락이 있으면 다른 앱에 Apple Events를 보낼 수 있습니다. 그렇게 대화할 수 있는 앱 중 하나가 단축어이고, 단축어에는 창을 찾고, 옮기고, 크기를 바꾸는 Find Windows, Move Window, Resize Window라는 동작이 있습니다. 이 동작들은 단축어 안에서, 단축어 자체의 권한으로, 제 샌드박스 밖에서 실행됩니다.
이 방법은 제가 알아낸 것이 아닙니다. Mac App Store에 있는 창 정렬 앱 snApp이 이미 이 동작들을 쓰는 단축어를 함께 제공하고 있습니다. 저는 그 단축어 파일을 읽고 형식을 익혔습니다.
그래서 App Store 버전의 Kolibri는 이렇게 작동합니다.
- 키를 누르고 있는 동안 Kolibri는 자체 윤곽선을 그리고, 마우스를 따라 그 윤곽선을 움직입니다. 실제 창은 제자리에 있습니다.
- 손을 떼면 Kolibri는 Shortcuts Events에 Apple Event 하나를 보냅니다. 이 이벤트는 "Kolibri Window Mover"라는 단축어를 작은 JSON 데이터와 함께 실행합니다. 데이터에는 앱 이름, 그 앱의 몇 번째 창인지, 새 프레임이 들어 있습니다.
- 단축어는 데이터를 읽고, Find Windows로 창을 찾은 뒤, Move Window와 Resize Window로 창을 배치합니다.
Apple Event는 AppleScript 한 줄입니다.
tell application "Shortcuts Events" to run shortcut "Kolibri Window Mover" with input "{\"app\":\"Safari\",\"i\":1,\"x\":0,\"y\":38,\"w\":1280,\"h\":771}"
샌드박스 외에 추가로 필요한 entitlement는 Apple Events entitlement와, 단축어 실행만 허용하고 그 밖의 것은 허용하지 않는 Shortcuts Events용 스크립팅 대상뿐입니다. 사용자는 처음 창을 옮길 때 자동화 권한 요청을 한 번 보게 됩니다.
단축어 자체는 저장소에 있는 Python 스크립트로 생성하고
shortcuts sign --mode anyone으로 서명합니다. Mac의 단축어
앱은 서명되지 않은 단축어 파일을 가져오지 않으며, "anyone" 모드로
서명하면 누구나 가져올 수 있습니다. 서명된 파일은 앱 안에 들어 있습니다.
처음 실행할 때 Kolibri는 shortcuts list를 실행하고,
단축어가 없으면 그 파일을 엽니다. 그러면 단축어 앱이 평소의 가져오기
화면을 보여 줍니다. 사용자는 한 번만 클릭하면 됩니다.
거절
처음 제출한 빌드는 9월 2일에 거절되었습니다. 손쉬운 사용을 쓴다는 이유로 가이드라인 2.4.5, 스크린샷 때문에 2.3.3이 적용되었습니다.
그 빌드에서도 창 이동은 이미 단축어를 거치고 있었습니다. 거절의 원인은 마우스를 따라가고 키를 읽는 방식이었습니다. 저는 CGEvent tap을 썼는데, 이것은 손쉬운 사용 권한이 필요합니다. 그래서 앱이 손쉬운 사용 권한을 요청했고, 손쉬운 사용 권한을 요청하는 창 도구는 그 권한으로 무엇을 하든 거절됩니다.
해결책은 그 권한이 필요 없게 만드는 것이었습니다. 지금 App Store 버전은 표준 전역 mouse-moved 이벤트 모니터로 포인터를 따라가고, 보조 키는 시스템의 현재 플래그 상태에서 읽습니다. 둘 다 손쉬운 사용 권한도, 입력 모니터링 권한도 필요 없습니다. 저는 tap과 권한 확인, 그 권한을 요청하던 메뉴 항목을 없앴고, 스크린샷은 드래그 도중의 윤곽선을 편집 없이 캡처한 화면으로 바꿨습니다. 두 번째 제출은 통과했습니다.
치르는 대가
창은 손을 뗀 뒤 200에서 330밀리초 후에 자리를 잡습니다. Apple Events와 단축어를 한 번 오가는 시간이고, 제가 줄일 방법은 없습니다. App Store 버전이 드래그하는 동안 창 자체 대신 윤곽선을 보여 주는 것도 이 때문입니다. 마우스 이벤트마다 단축어로 실제 창을 옮기면 너무 느립니다.
또 단축어는 핸들이 아니라 앱 이름과 창 순서로 창을 찾습니다. 실제로는 믿을 만하지만, 손쉬운 사용으로 얻는 정보에 비하면 추측입니다.
그리고 키를 누른 채 클릭해야 드래그가 시작되는 클릭 모드에서는 App Store 버전이 그 클릭을 가로챌 수 없습니다. 그것도 event tap이 필요하기 때문입니다. 클릭은 아래에 있는 창에 그대로 전달됩니다.
두 가지 빌드
윤곽선은 괜찮은 절충안이지만, 제가 처음에 만들려던 앱은 아닙니다. 그래서 빌드가 두 가지입니다.
App Store 버전은 위에서 설명한 것입니다. Kolibri Pro는 이 사이트에서 따로 받는 앱으로, Developer ID로 서명하고 공증을 받았으며 샌드박스가 적용되지 않습니다. 손쉬운 사용을 이용해, 원래의 Kolibri처럼 실제 창을 내용이 보이는 채로 실시간으로 움직입니다. 단축어도, 자동화 권한 요청도 필요 없습니다.
Pro는 App Store 버전을 구입한 사람이라면 누구나 무료이고, 라이선스 키도, 계정도, 서버도 없습니다. Pro는 실행될 때 같은 Mac에 있는 App Store 버전의 Kolibri를 찾아 그 영수증을 오프라인으로 확인합니다.
- 영수증의 서명은 Apple 루트 인증서까지 이어져야 합니다. Pro는 시스템 인증서 저장소를 믿는 대신 이 인증서를 직접 가지고 있습니다.
- 영수증에는 App Store 앱의 번들 ID가 적혀 있어야 합니다.
- 영수증의 해시가 이 Mac의 하드웨어 ID와 일치해야 합니다. 그래서 다른 Mac에서 옮겨 온 사본은 App Store에서 다시 다운로드하기 전까지 Pro를 잠금 해제하지 못합니다.
이 중 하나라도 실패하면 Pro는 이유를 알려 주고, App Store를 열 수 있게 한 뒤 종료합니다. 결제와 환불은 App Store가 처리하고, 실시간으로 움직이는 버전을 원하는 사람도 그 버전을 쓸 수 있습니다. 두 빌드는 하나의 Xcode 프로젝트에서 나옵니다. 포인터 추적, 키, 설정, 메뉴 막대는 같고, 창을 움직이는 부분만 다릅니다.
빌드를 두 가지로 내놓는다는 아이디어는 ATP에서 John Siracusa의 이야기를 듣다가 얻었습니다.
두 빌드를 나란히 보여 주는 영상은 프레스 키트에 있습니다.