Text Selection & Strategy Chain
Multi-strategy selection extraction using Accessibility APIs, WebKit markers, and AppleScript.
On this page (8 sections)
The text selection subsystem detects user text selection events across all macOS applications and extracts selected text non-destructively, then hands it to the popup.
Selection Detection Architecture#
The subsystem consists of three primary components:
+------------------------+ +-----------------------+
| MacSelectionMonitor |-->| RuleEngine |
| (Global Mouse/AX Event)| | (Per-App Policy) |
+------------------------+ +-----------------------+
| |
| builds v
| +-----------------------+
| | SelectionContext |
| +-----------------------+
v |
+------------------------+ |
| SelectionRetrievalCoordinator | |
| (Gate + Mode Routing) | |
+------------------------+ |
v
+-----------------------+
| onSelection callback |
| (PopupWindowController) |
+-----------------------+MacSelectionMonitor: Listens for mouse release events (leftMouseUp) and keyboard selection gestures (⌘A select-all, ⇧+arrow) and dispatches retrieval.SelectionRetrievalCoordinator(fromOpenSelectionpackage viaOpenSelectionBridge): Applies the gate, resolves the app's retrieval mode fromAppPolicyContext, and routes to the matching strategy.- Context assembly:
MacSelectionMonitorresolves app rules viaRuleEngine, builds aSelectionContext, and notifies subscriber callbacks (such asPopupWindowController).
The monitor snapshots the window under a mouse press and rejects the release if that window
moved, resized or disappeared. This avoids retrieving an old selection from the editor that
remains focused while its title bar is dragged. New presses and switches away from the source
process cancel pending automatic reads and clear the shortcut cache. Queued notifications for
apps no longer frontmost, the source app's own activation, and OpenClip's popup activation are
ignored. The monitor rechecks the source PID before caching or presenting results after async
work. At the copy boundary, OpenSelection's AutomaticCopyCapture
requires an enabled Command-C menu shortcut, then rechecks cancellation, the frontmost process
and the overlay guard before invoking the copy trigger. The bounded menu probe runs off the
main actor. Native AX retrieval and the explicit hotkey use their existing behavior. See
current limitations.
The shared capture and window-geometry implementations live in OpenSelection. OpenClip retains only its app-specific monitor lifecycle (including hold-to-trigger and cached selections).
Retrieval: Resolver, Gate, and Strategies#
Every retrieval request (mouse-up drag, ⌘A/⇧+arrow gesture, or the ⌥⌘C hotkey) runs through SelectionRetrievalCoordinator.retrieve(for:policy:cursor:):
- Fresh AX snapshot —
AXElementInspector.inspect()resolves the focused application, then the focused UI element from that application, never from the system-wide element (the classic source of stale reads). It collects the role, parent/container roles, selection attributes, and selection bounds. The blocking snapshot runs on the dedicatedcom.openclip.ax-inspectqueue, raced againstConstants.axReadTimeout(0.5 s) via a once-resume gate; a hung or unresponsive target yieldsnilinstead of stalling the popup. - Gate —
SelectionGatePolicydecides whether to attempt retrieval at all:skipRoles— AX roles that can never hold a text selection (buttons, menus, scrollbars, …) are rejected up front.allowedCursors— the cursor class (fromCursorClassifierinOpenSelection) must suggest a text context;.unknownis never a reason to block.
- Strategy chain — a single canonical fallback order selects the first working strategy. The app's
SelectionRetrievalModepicks the entry point into that chain; retrieval then runs that strategy and every strategy below it. An app with no rule starts atax-text-control(the top), which is the "auto" behavior.
The canonical chain:
ax-text-control → ax-web-area → menu-copy → keyboard-copyChain suffix per preferred mode:
Web selections run directly through native Accessibility (AXWebArea), reading kAXSelectedTextMarkerRange in <1 ms without spawning subprocesses or requiring Apple Events permissions. If Accessibility yields no text (e.g. Google Docs canvas or custom editors), the coordinator falls back to keyboard-copy.
Retrieval modes#
The copy engine and transient markers#
Both copy modes run through PasteboardCopyEngine: archive every type of every pasteboard item → run the trigger → poll every 2 ms up to a per-app timeout (pasteboardCopyTimeout 0.25 s, or safariPasteboardCopyTimeout 0.6 s for browsers) for an advanced changeCount yielding non-empty text (a changeCount advance with empty content keeps polling, covering the transient clipboard race; recopying identical text succeeds as long as changeCount advances) → read the new string → restore the archived items synchronously before returning, tagged with the nspasteboard markers org.nspasteboard.TransientType and org.nspasteboard.AutoGeneratedType (empty data). The markers tell clipboard managers to skip the restore as a user-visible copy. The clipboard is therefore clean by the time retrieval returns; there is no lingering visibility window.
Per-app routing (default catalog)#
DefaultAppRules.catalog assigns modes to app groups; RuleEngine.resolvePolicies matches the frontmost app's identity (bundle ID, or a process:/path: identifier for bundle-less apps) against default + user rules (with .* prefix / * wildcards). User rules in ~/.openclip/rules.json override per-key — see docs/user-guide/app-rules.md for the JSON keys.
Shortcut Clipboard Fallback & Synchronous Resolution#
The retrieval path above applies to passive selection monitoring. The global toggle shortcut (HotkeyManager) runs a strictly synchronous resolution pipeline on @MainActor without incurring asynchronous AX query latency:
- Monitored Selection Reuse: The hotkey checks
selectionMonitor.synchronousSelection(for: frontmostBundleID). If the user recently selected text in the active application and that selection has not expired (Constants.selectionMaxAge= 30 s) or been cleared by caret navigation / typing, the monitored selection is reused immediately. - Clipboard Fallback: If no valid monitored selection exists, OpenClip falls back to the current contents of
NSPasteboard.generalso the search palette still has input to act on.- The context is flagged
SelectionContext.isClipboardFallback;PopupWindowController.showfilters available actions down to Paste (and AI Tools launcher).
- The context is flagged
- Empty Context Fallback: If the clipboard is also empty, an empty selection context is created with the frontmost app's identity, allowing standalone actions to run.
Passive selection monitoring continues even when "Appear Automatically" is disabled (isAppEnabled == false or hotkeyOnly: true), updating latestSelection and pre-warming the search index in the background so pressing the shortcut opens the palette with zero perceptual delay. Both passive mouse and keyboard paths withhold synthetic copy fallback under those settings; native AX reads still populate the cache. The explicit hold gesture delivers straight from handleMouseDown and stays unaffected by isAppEnabled.
The hold's editable-field lookup runs off the main actor through SelectionEditabilityProbe,
bound to the original source PID and one axReadTimeout deadline. If the lookup times out,
clipboard fallback is refused; the UI thread keeps processing events.
Privacy & Non-Destructive Guarantees#
- No Clipboard Pollution (AX modes):
ax-text-control,ax-web-area, andbrowser-scriptnever write toNSPasteboard— they read the live accessibility tree or the browser's AppleScript bridge. - Copy modes are archive-and-restore:
menu-copy/keyboard-copytemporarily place the selected text on the general pasteboard, then restore the archived items tagged with the nspasteboard transient markers so clipboard managers don't treat the restore as a user copy. - Ignored Fields: Secure text fields (such as password inputs or masked text areas) do not expose
kAXSelectedTextAttributethrough AX APIs, ensuring password security.
Inline Action Match Context#
Inline result evaluation receives the same per-action regex match context as click execution. The
selection remains available as openclip.input.text, while openclip.input.matchedText and
openclip.input.captures carry the regex match and capture groups. This lets inline actions such as
Currency Converter evaluate a value embedded in a longer selected passage.