C.W.K.
Stream
Lesson 04 of 05 · published

되돌릴 수 있는 복사 프로브

~12 min · copy-probe, cgevent, changecount, reversible

Level 0차가운 부싯돌
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"선택을 못 읽으면, 앱한테 복사하라고 시켜 — 그다음 클립보드를 찾은 그대로 정확히 돌려줘."

클립보드를 빌리고, 도로 줘

AX 가 선택을 못 볼 때, 모든 앱이 이해하는 게 하나 있어: Cmd+C. Flint 가 복사를 합성하면, 불투명 앱이 선택된 텍스트를 클립보드에 올리고, Flint 가 거기서 읽을 수 있어. 함정은 뻔해 — 그게 사용자가 클립보드에 갖고 있던 걸 뭉개. 그래서 프로브는 되돌릴 수 있게 지어져: 클립보드를 스냅샷하고, 복사 한 번을 위해 빌리고, 결과를 읽고, 원본을 복원해. 제대로 하면, 사용자 클립보드는 손대지 않고 빌린 걸 절대 몰라.

Cmd+C 합성하기

복사 자체는 합성 키 입력이야. Command 플래그가 설정된 C 키의 키-다운과 키-업 이벤트를 만들어 이벤트 스트림에 게시해, 사용자가 Cmd+C 를 누른 것처럼 정확히. 최전면 앱이 그걸 받고 복사에 늘 하는 걸 해. Flint 는 앱 내부를 파싱하는 게 아냐 — 앱이 이미 구현한 하나의 보편 제스처를 쓰고, 결과가 떨어지는 표준 자리를 읽는 거야.

복사는 비동기야 — 클립보드를 즉시 읽지 마. 합성 Cmd+C 를 게시한 뒤, 타깃 앱이 자기 스케줄로, 몇 밀리초 뒤에 클립보드를 써. 게시 직후에 NSPasteboard 를 읽으면, 내용을 얻고 프로브가 실패했다고 생각해. 맞는 패턴은 페이스트보드의 changeCount 를 먼저 기록하고, 증가할 때까지 기다리는 거야(짧은 타임아웃과 함께) — 그 증가가 복사가 실제로 일어났다는 네 증명이야.

되돌릴 수 있다 = 복원됐다

이 프로브가 받아들여지는 이유 전부는 복원 단계야. 복사 전에, Flint 가 완전한 현재 페이스트보드를 캡처해. 프로브된 텍스트를 읽은 뒤, 캡처한 상태를 도로 써. 복사가 아무것도 안 만들었으면(changeCount 가 안 움직임), Flint 는 어쨌든 복원하고 선택 없음을 보고해 — 성공이든 실패든, 클립보드를 빌린 상태로 절대 안 남겨. 다음 레슨은 그 캡처-복원을 완전히 하는 거야, '클립보드 복원' 은 들리는 것보다 어려우니까.

Code

앱이 진짜와 구분 못 하는 Cmd+C 합성·swift
func synthesizeCopy() {
    let src = CGEventSource(stateID: .combinedSessionState)
    let cKey: CGKeyCode = 0x08                 // kVK_ANSI_C

    let down = CGEvent(keyboardEventSource: src, virtualKey: cKey, keyDown: true)
    down?.flags = .maskCommand                 // Cmd 눌림
    let up = CGEvent(keyboardEventSource: src, virtualKey: cKey, keyDown: false)
    up?.flags = .maskCommand

    down?.post(tap: .cghidEventTap)
    up?.post(tap: .cghidEventTap)
}
되돌릴 수 있는 프로브: 스냅샷, 복사, 대기, 읽기, 복원·swift
func probeSelectionViaCopy() -> String? {
    let pb = NSPasteboard.general
    let before = pb.changeCount
    let saved = capturePasteboard(pb)   // 완전 스냅샷 (다음 레슨)

    synthesizeCopy()

    // 앱이 실제로 클립보드를 쓸 때까지 대기. changeCount 증가가
    // 증명; 즉시 읽으면 옛 내용을 읽게 됨.
    let deadline = Date().addingTimeInterval(0.4)
    while pb.changeCount == before, Date() < deadline { usleep(8_000) }

    let captured = pb.changeCount != before ? pb.string(forType: .string) : nil

    restorePasteboard(pb, from: saved)  // 되돌림: 항상 도로 넣음
    return captured
}

External links

Exercise

복사 프로브의 실패 경우들과 각각의 맞는 대응을 써: (a) 앱이 복사를 안 함(changeCount 안 움직임), (b) 앱이 복사했지만 텍스트가 빔, (c) 프로브 중간에 에러 던져짐. 셋 다에서, 함수가 반환할 때 사용자 클립보드에 대해 어떤 불변식이 성립해야 해? 코드 어디서 보장해?
Hint
모든 경우 — 복사 안 일어남, 복사가 빔, 예외 던져짐 — 에서 불변식은: 클립보드가 시작한 그대로 끝난다. 프로브 전에 캡처하고 모든 출구 경로에서 복원해서(defer 스타일 복원, 또는 모든 반환 전 단일 복원) 보장해, 어떤 이른 반환도 빌린 클립보드를 남기지 못하게. 읽기 결과는 다양해도; 복원은 안 그래.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.