TL;DR

  • Riffle은 macOS에서 Option-Tab으로 현재 가장 앞에 있는 앱의 창을 전환하며, 최소 구성의 메뉴 막대 전용 도구임.
  • 최소화된 창과 다른 Spaces의 창까지 포함하고, 최근 사용 순서대로 표시함.
  • 검색 모드에서는 폴더·창 제목·앱 이름으로 목록을 필터링하며, 패널은 키보드 포커스를 가져가지 않음.
  • Swift 약 500줄, 의존성 없음이며, 창 탐색과 키 입력 가로채기에 접근성 권한을 사용함.
  • macOS 13 이상과 Xcode 명령줄 도구가 필요하고, 현재 앱 내부 전환만 지원하며 단축키 설정과 미리보기 이미지는 제공하지 않음.

Riffle

  • 현재 앱의 창 사이를 전환하는 최소한의 키보드 창 전환기임.
  • 메뉴 막대 전용이며 Dock 아이콘이 없고, Swift 약 500줄로 구성되며 의존성은 없음.

기능

  • ⌥을 누른 채 ⇥을 누르면 가장 앞에 있는 앱의 모든 창을 제목 목록으로 순환함.
  • 접근성 API(Accessibility API)를 통해 최소화된 창과 다른 Spaces의 창도 포함함.
  • 창을 앞에서 뒤 순서로 정렬하며, 가장 최근에 사용한 창이 먼저 표시됨.
  • 패널 크기를 가장 긴 제목에 맞추고 마우스 포인터가 있는 화면의 중앙에 배치함.
  • 마우스로 항목에 포인터를 올려 선택하고 클릭해 포커스할 수 있음.
  • 메뉴 막대에서 Stay Open to Search를 켜면 ⌥⇥을 짧게 눌렀을 때 패널이 열린 상태로 남아 목록을 검색할 수 있음.

키

  • ⌥ ⇥ / ⌥ ↓ / ⌥ →: 전환기를 열거나 다음 창으로 이동함.
  • ⌥ ⇧ ⇥ / ⌥ ↑ / ⌥ ←: 이전 창으로 이동함.
  • ⌥에서 손을 떼거나 ⏎을 누르면 선택한 창에 포커스함.
  • ⎋: 취소함.

검색 모드

  • 메뉴 막대에서 Stay Open to Search를 켠 상태로 ⌥⇥을 누른 뒤 250밀리초 이내에 ⌥에서 손을 떼면 창 전환을 확정하는 대신 검색 필드가 있는 패널을 열어 둠.
  • ⌥을 250밀리초 넘게 계속 누르면 기존 방식으로 전환기가 유지되며, ⇥으로 창을 순환하고 ⌥에서 손을 떼면 강조된 창에 포커스함.
  • 입력한 모든 검색어가 순서와 관계없이 일치해야 하며, 폴더·창 제목·앱 이름으로 목록을 필터링함.
  • ⇥ / ⇧⇥ / ↑↓: 선택 항목을 이동함.
  • ⌫: 문자 하나를 삭제함.
  • ⏎ 또는 클릭: 선택한 창에 포커스함.
  • ⎋: 검색어를 지운 뒤 취소함.
  • 패널은 키보드 포커스를 가져가지 않으며, 이벤트 탭(event tap)에서 키 입력을 읽고 가로채므로 아래 앱으로 입력이 전달되지 않음.
  • 패널 바깥을 클릭하거나 ⌘/⌃ 단축키를 누르면 패널이 닫힘.

빌드 및 설치

  • macOS 13 이상과 Xcode 명령줄 도구가 필요함.
  • 빌드 스크립트는 build/Riffle.app을 생성하며, 앱을 열거나 먼저 /Applications로 복사해 설치할 수 있음.
  • build.sh는 사용 가능한 첫 번째 코드 서명 ID로 서명함.
  • macOS는 접근성 권한을 코드 서명에 연결하며, 애드혹 서명은 빌드할 때마다 달라지므로 인증서를 사용하면 다시 빌드해도 권한을 유지할 수 있음.

권한

  • 접근성 권한이 필수이며, 전역 ⌥⇥ 입력 가로채기, 창 열거, 창 앞으로 가져오기에 사용함.
  • 첫 실행 때 권한 요청이 표시되며, 시스템 설정 → 개인정보 보호 및 보안 → 손쉬운 사용에서 허용해야 함.
  • 화면 기록 권한은 필요하지 않음.

아키텍처

  • main.swift에서 AppDelegate로 이어지고, AppDelegate는 ⌥⇥을 가로채는 KeyboardHook(CGEventTap)을 사용함.
  • 상태 메뉴 항목은 현재 세션 상태와 가장 앞에 있는 앱의 범위를 관리하는 Switcher와 연결됨.
  • Switcher 구성 요소는 다음과 같음.
  • WindowDiscovery: 접근성(AX) 창과 CG의 창 순서 정보를 결합함.
  • SwitcherPanel: 활성화하지 않는 NSPanel임.
  • SwitcherView: 제목 목록을 표시하는 SwiftUI 뷰임.
  • WindowFocus: 접근성 API로 창의 최소화를 해제하고 창을 앞으로 가져온 뒤 앱을 활성화함.
  • CGEventTap은 ⌥⇥을 포커스된 앱에 전달되기 전에 가로챔.
  • 창과 AX 요소의 연결에는 비공개 함수 _AXUIElementGetWindow를 사용하고, 창 순서는 CGWindowListCopyWindowInfo에서 가져옴.
  • 패널은 테두리 없는 .nonactivatingPanel이므로 포커스를 가져가지 않음.

제한 사항

  • 현재 앱 안에서만 창을 전환하며, 앱 간 전환 모드는 없음.
  • 단축키를 설정할 수 없으며, 미리보기 이미지는 제공하지 않음.

라이선스

  • MIT 라이선스임.