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

The Reversible Copy Probe

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

Level 0Cold Flint
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"If you can't read the selection, ask the app to copy it — then hand the clipboard back exactly as you found it."

Borrow the Clipboard, Then Give It Back

When AX can't see the selection, there's one thing every app understands: Cmd+C. If Flint synthesizes a copy, the opaque app puts the selected text on the clipboard, and Flint can read it there. The catch is obvious — that clobbers whatever the user had on the clipboard. So the probe is built to be reversible: snapshot the clipboard, borrow it for one copy, read the result, and restore the original. Done right, the user's clipboard is untouched and they never know it was borrowed.

Synthesizing Cmd+C

The copy itself is a synthetic keystroke. You build a key-down and key-up event for the C key with the Command flag set and post them to the event stream, exactly as if the user had pressed Cmd+C. The frontmost app receives it and does what it always does with a copy. Flint isn't parsing the app's internals — it's using the one universal gesture the app already implements, and reading the standard place the result lands.

The copy is asynchronous — do not read the clipboard immediately. After you post the synthetic Cmd+C, the target app writes the clipboard on its own schedule, a few milliseconds later. If you read NSPasteboard the instant after posting, you'll get the old contents and think the probe failed. The correct pattern is to record the pasteboard's changeCount before, then wait until it increments (with a short timeout) — the bump is your proof the copy actually happened.

Reversible Means Restored

The whole reason this probe is acceptable is the restore step. Before the copy, Flint captures the complete current pasteboard. After reading the probed text, it writes that captured state back. If the copy never produced anything (changeCount never moved), Flint restores anyway and reports no selection — it never leaves the clipboard in a borrowed state, success or failure. The next lesson is about doing that capture-and-restore completely, because 'restore the clipboard' is harder than it sounds.

Code

Synthesize a Cmd+C the app can't tell from a real one·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 held
    let up = CGEvent(keyboardEventSource: src, virtualKey: cKey, keyDown: false)
    up?.flags = .maskCommand

    down?.post(tap: .cghidEventTap)
    up?.post(tap: .cghidEventTap)
}
The reversible probe: snapshot, copy, wait, read, restore·swift
func probeSelectionViaCopy() -> String? {
    let pb = NSPasteboard.general
    let before = pb.changeCount
    let saved = capturePasteboard(pb)   // FULL snapshot (next lesson)

    synthesizeCopy()

    // Wait for the app to actually write the clipboard. The changeCount
    // bump is the proof; reading immediately would read the OLD contents.
    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)  // reversible: always put it back
    return captured
}

External links

Exercise

Write the failure cases for the copy probe and the correct response to each: (a) the app never copies (changeCount doesn't move), (b) the app copies but the text is empty, (c) an error is thrown mid-probe. In all three, what invariant must hold about the user's clipboard when the function returns? Where in the code do you guarantee it?
Hint
In every case — copy never happened, copy was empty, exception thrown — the invariant is: the clipboard ends exactly as it started. You guarantee it by capturing before the probe and restoring on every exit path (a defer-style restore, or a single restore before every return), so no early return can leave the borrowed clipboard behind. The read result varies; the restore does not.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.