맥 화면 위에 항상 떠있는 집중 타이머. 행동을 막지는 않고, 보이게 해서 망설이게 만든다.
An always-on-top focus timer for macOS that doesn't block anything — it just watches, turns red when you drift, and keeps the count where you can see it.
화면 구석에 데스크 펫이 앉아서 같이 일한다. 딴짓하면 화낸다.
╭─────────────────╮ ╭─────────────────╮
│ ◔ 24:13 │ │ ● 24:13 ⏸ 멈춤 │
╰─────────────────╯ ╰─────────────────╯
╭──────────╮ ╭──────────────╮
│ 잘하고 있어 │ │ youtube.com?! │
╰─────▽────╯ ╰──────▽───────╯
ᐟ\_( •ᴥ• )_/ᐟ ᐠ( •̀ Ĺ̯ •́ )ᐟ 💢
같이 일하는 중 화남 + 타이머 정지
앱 차단기는 결국 뚫는다. 예외를 추가하거나, 꺼버리거나, 폰으로 옮겨간다. 그래서 막는 대신 눈에 띄는 자리에 계속 떠 있게 했다. 유튜브를 열 수는 있는데, 그 순간 화면 위 알약이 빨개지고 흘린 시간이 초 단위로 올라간다. 막지 않으니 반발도 없고, 보이니까 그냥 하기가 좀 그렇다.
- macOS 13 (Ventura) 이상 — 개발/테스트는 macOS 15.6
- Xcode Command Line Tools (
xcode-select --install) —swiftc만 있으면 됨
Xcode 앱 자체나 외부 패키지는 필요 없다. 의존성 0개.
git clone https://github.com/jaylions/AttentionTimer.git
cd AttentionTimer터미널 없이 쓰는 법 (권장) — 한 번만:
./install.sh/Applications/AttentionTimer.app 으로 설치된다. 이후로는
- Launchpad / Spotlight(⌘Space)에서 "AttentionTimer" 로 실행
- 또는 Finder에서 더블클릭
- 로그인할 때 자동 실행 → 메뉴바
◷→ 로그인 시 자동 실행 체크
한 번 켜두면 그냥 계속 떠있는 물건이라 사실 자동 실행 켜놓는 게 제일 편하다.
개발용 (코드 고칠 때):
./build.sh # 컴파일
./run.sh # 빌드된 걸 실행Xcode 프로젝트 없음. swiftc 한 줄로 .app 번들이 만들어진다.
/Applications에 설치돼 있으면 build.sh가 그 사본도 자동으로 같이 갱신한다.
| 하는 일 | 쓰는 것 | 권한 |
|---|---|---|
| 지금 맨 앞 앱이 뭔지 | NSWorkspace.didActivateApplicationNotification |
필요 없음 |
| 브라우저 열린 탭 주소 | osascript 한 줄 (AppleScript) |
첫 실행 때 "자동화 허용" 1회 |
| 딴짓 판정 | 번들 ID / URL 문자열 단순 대조 | — |
네트워크 요청 0. 주소는 로컬에서 문자열 비교만 하고 버린다.
지원 브라우저 (탭 URL 읽기): Dia, Arc, Chrome, Safari, Edge, Brave, Vivaldi, 웨일. Firefox는 AppleScript로 URL을 노출하지 않아 앱 단위 감지만 된다.
⚠️ ./build.sh로 다시 빌드하면 앱 서명이 바뀌어서 "자동화 허용" 팝업이 다시 뜰 수 있다. 코드 고칠 때마다 한 번씩 허용해주면 됨.
하얗고 복슬복슬한 강아지. 상태에 따라 표정·귀·앞발이 바뀐다.
전부 코드로 그린다 — 이미지 파일 한 장 없이 NSBezierPath 로만.
털 실루엣은 원의 반지름을 각도에 따라 흔들어서 만든다 (fluff()).
| 상태 | 강아지 | 말풍선 |
|---|---|---|
| 대기 | 눈 감고 꾸벅꾸벅, 귀 축 늘어짐, z |
"오늘도 해볼까?" |
| 집중 중 | 눈 뜨고 앞발을 번갈아 움직임 (타자 치는 동작) | "잘하고 있어" / "절반 왔다" / "5분 남았어" |
| 딴짓 감지 | 눈썹 치켜뜨고 귀 쫑긋, 입 헤벌린 채 부들부들, 💢 | "야." "어디 가" "지금 뭐 하는데" … |
| 완료 | 눈이 ^ ^, 앞발 번쩍 들고 폴짝폴짝 |
"한 번도 안 흔들렸어!" |
- 항상 최상단 — 풀스크린 유튜브 위에도 뜬다 (
.screenSaver레벨 +fullScreenAuxiliary) - 모든 데스크탑(Space)에 따라다님
- 투명한 부분은 클릭이 통과한다 — 캐릭터·말풍선·타이머 밖은 뒤쪽 앱이 그대로 눌린다
- 드래그로 위치 이동 — 위치는 저장됨. 되돌리려면 메뉴바 → 알약 위치 초기화
- 클릭해도 작업 앱 포커스를 뺏지 않음 (
nonactivatingPanel) - 캐릭터를 클릭 = 시작/일시정지. 타이머 위에 마우스를 올리면 ▶ / ↺ / ✕ 버튼
- CPU 약 1.8% (대기 중). 상태에 따라 8~30Hz로 타이머 주기를 바꿔서 불필요하게 안 깨운다
- 앞에 온 앱이
distractionApps에 있거나, 브라우저 탭 URL이distractionSites의 문자열을 포함하면 → 후보 - 6초(
graceSeconds) 넘게 머물러야 실제 경고. 잠깐 스치는 건 봐준다 - 경고 발동 → 캐릭터가 화내고, 타이머가 멈춘다,
흔들림 N회카운터 +1 - 돌아오면 즉시 원상복구되고 타이머도 다시 간다. 단 카운터는 안 지워진다
이게 핵심이다. 빨간 상태에서는 남은 시간이 1초도 줄지 않는다. 25분 세션 중에 10분을 유튜브에 쓰면 그 세션은 35분 걸린다. 돌아오지 않으면 세션은 영원히 끝나지 않는다 — 도망칠 수가 없다.
집중 중(focusing)일 때만 감지한다. 일시정지·대기 중엔 아무것도 안 본다.
JSON 안 건드려도 된다. 메뉴바 ◷ 아이콘을 누르면:
집중 시작 (25분)
─────────────────────────────
오늘 흔들림 2회 · 4분
─────────────────────────────
+ 딴짓에 추가: KakaoTalk ← 지금 앞에 있던 앱
+ 딴짓에 추가: youtube.com ← 지금 보던 사이트
딴짓 목록 관리 ▸ ← 클릭해서 빼기
─────────────────────────────
기록 파일 열기
설정 파일 열기 (JSON)
...
☑ 로그인 시 자동 실행
딴짓하다 걸렸을 때 메뉴바만 누르면 "+ 딴짓에 추가: 그거" 가 이미 떠 있다.
메뉴를 열어도 직전에 보던 앱/사이트를 기억하기 때문 (자기 자신은 무시).
딴짓 목록 관리 ▸ 안에서 항목을 클릭하면 목록에서 빠진다.
바꾼 내용은 바로 config.json에 저장되고 즉시 적용된다.
~/.attentiontimer/config.json — 메뉴바 → 설정 파일 열기 / 고친 뒤 설정 다시 읽기
| 키 | 뜻 |
|---|---|
focusMinutes |
한 세션 길이 (기본 25) |
graceSeconds |
딴짓 유예 시간 (기본 6초) |
watchBrowserURL |
브라우저 탭 감시 on/off. false면 자동화 권한 아예 안 씀 |
logAllSites |
true로 하면 집중 중 방문한 모든 사이트를 기록에 남김 (기본 false) |
distractionApps |
차단 앱 번들 ID 목록 |
distractionSites |
차단 사이트 문자열 (URL에 포함되면 걸림) |
내 앱의 번들 ID 찾기:
./ids.sh~/.attentiontimer/events.jsonl — 한 줄에 한 이벤트, 그냥 텍스트라 나중에 뭘로든 분석 가능.
{"day":"2026-08-24","type":"distraction_start","app":"Google Chrome","detail":"youtube.com","t":"..."}
{"day":"2026-08-24","type":"distraction_end","app":"Google Chrome","seconds":73,"t":"..."}
{"day":"2026-08-24","type":"session","completed":true,"planned_sec":1500,"distracted_sec":120,"warnings":3,"t":"..."}
{"day":"2026-08-24","type":"site","host":"github.com","app":"Dia","t":"..."} // logAllSites=true 일 때만logAllSites를 켜면 집중 시간에 들른 사이트가 호스트 단위로 남는다 (호스트가 바뀔 때만 한 줄, 전체 URL은 저장 안 함).
나중에 "내가 집중한다며 어디를 돌아다녔나" 보기 좋음:
grep '"type":"site"' ~/.attentiontimer/events.jsonl | python3 -c '
import sys,json,collections
c=collections.Counter(json.loads(l)["host"] for l in sys.stdin)
for h,n in c.most_common(20): print(f"{n:4} {h}")'오늘 흔들린 횟수/시간은 앱 시작할 때 이 파일에서 다시 읽어온다. 껐다 켜도 안 리셋됨.
src/main.swift 전부 여기 (Swift + AppKit, ~700줄)
build.sh swiftc → AttentionTimer.app 번들
install.sh /Applications 로 설치
run.sh 빌드된 앱 실행
ids.sh 실행 중인 앱들의 번들 ID 출력
MIT. 마음대로 가져다 쓰세요.