selection-capture 0.1.3

Sync, cancellable selected-text capture engine with strategy-aware fallbacks
Documentation

selection-capture

Crates.io Documentation CI License

selection-capture is a Rust library for selected-text capture with retry, cancellation, and strategy fallbacks.

It is designed as a replacement foundation for earlier get-selected-text style usage, with explicit capture status and trace metadata for app-level UX decisions.

Features

  • โœ… Synchronous API - Simple, blocking calls that are easy to integrate
  • โœ… Optional Async API - Feature-gated capture_async(...) for Tokio-based applications
  • โœ… Optional Rich Content API - Feature-gated capture_rich(...) / try_capture_rich(...) with clipboard HTML/RTF enrichment
  • ๐Ÿงช Experimental Monitoring Scaffold - Backend-agnostic monitoring API surface for future selection change streams
  • ๐Ÿ”„ Retry Logic - Automatic retry with configurable budgets and delays
  • โšก Multiple Strategies - Falls back through different capture methods automatically
  • ๐ŸŽฏ App-Specific Profiles - Customize behavior per application
  • ๐Ÿ” Detailed Tracing - Full visibility into what happened during capture attempts
  • โŒ Cancellation Support - Cooperative cancellation via CancelSignal trait
  • ๐Ÿงน Automatic Cleanup - Clipboard cleanup after capture

Platform Support

  • macOS: Fully implemented (MacOSPlatform)
  • Windows: windows-beta includes real clipboard reads, foreground active-app lookup, and initial UIA + legacy IAccessible focused-element capture paths
  • Linux: linux-alpha includes shell-backed clipboard/primary-selection reads, active-app lookup, and AT-SPI focused-descendant capture
  • Other platforms: Portable API via CapturePlatform trait, implementations welcome!

Experimental monitoring support is scaffolded with a generic MonitorPlatform trait plus CaptureMonitor<P>, and CaptureMetrics for aggregating capture-outcome success/latency statistics from trace data. Polling monitor backends are available for macOS (MacOSSelectionMonitor), Windows (WindowsSelectionMonitor, windows-beta), and Linux (LinuxSelectionMonitor, linux-alpha) with per-backend de-duplication. Poll helpers now include cancellation-aware loops and an optional coalescing mode for bursty event streams.

Installation

Add this to your Cargo.toml:

[dependencies]
selection-capture = "0.1"

Or use the latest from Git:

[dependencies]
selection-capture = { git = "https://github.com/maemreyo/selection-capture" }

Enable the Windows beta scaffold explicitly:

[dependencies]
selection-capture = { version = "0.1", features = ["windows-beta"] }

Enable the optional async wrapper explicitly:

[dependencies]
selection-capture = { version = "0.1", features = ["async"] }

Enable rich-content capture explicitly:

[dependencies]
selection-capture = { version = "0.1", features = ["rich-content"] }

Quick Start (macOS)

use selection_capture::{
    capture, AppAdapter, AppProfile, AppProfileStore, AppProfileUpdate, CaptureOptions,
    CaptureOutcome, MacOSPlatform, CancelSignal, ActiveApp,
};

// Implement required traits for your use case
struct NeverCancel;
impl CancelSignal for NeverCancel {
    fn is_cancelled(&self) -> bool { false }
}

struct NoopStore;
impl AppProfileStore for NoopStore {
    fn load(&self, app: &ActiveApp) -> AppProfile { AppProfile::unknown(app.bundle_id.clone()) }
    fn merge_update(&self, _app: &ActiveApp, _update: AppProfileUpdate) {}
}

fn main() {
    let platform = MacOSPlatform::new();
    let store = NoopStore;
    let cancel = NeverCancel;
    let adapters: [&dyn AppAdapter; 0] = [];
    let options = CaptureOptions::default();

    match capture(&platform, &store, &cancel, &adapters, &options) {
        CaptureOutcome::Success(ok) => println!("Selected text: {}", ok.text),
        CaptureOutcome::Failure(err) => eprintln!("Capture failed: {:?}", err.status),
    }
}

Core API

Main Function

  • capture(...) โ†’ CaptureOutcome - The primary capture function
  • try_capture(...) โ†’ Result<CaptureOutcome, WouldBlock> - Non-blocking single-pass variant
  • capture_async(...).await โ†’ CaptureOutcome - Optional Tokio-backed wrapper behind the async feature
  • capture_rich(...) โ†’ CaptureRichOutcome - Optional rich-content capture behind rich-content
  • try_capture_rich(...) โ†’ Result<CaptureRichOutcome, WouldBlock> - Non-blocking rich variant behind rich-content

Configuration

  • CaptureOptions - Configure timeouts, trace collection, and strategy overrides
  • CapturePlatform - Trait for platform-specific implementations
  • MonitorPlatform - Experimental trait for platform-specific selection-change monitoring
  • CancelSignal - Trait for cooperative cancellation
  • AppAdapter - Trait for app-specific customizations
  • AppProfileStore - Trait for persisting app profiles

Experimental Monitoring

use selection_capture::{
    CancelSignal, CaptureMetrics, CaptureMonitor, MacOSMonitorBackend, MacOSSelectionMonitor,
    MacOSNativeEventSource, MacOSSelectionMonitorOptions, MonitorPlatform, MonitorSpamGuard,
};

struct StubMonitor;

impl MonitorPlatform for StubMonitor {
    fn next_selection_change(&self) -> Option<String> {
        Some("example selection".to_string())
    }
}

let monitor = CaptureMonitor::new(StubMonitor);
assert_eq!(monitor.next_event(), Some("example selection".to_string()));
let processed = monitor.run_with_limit(10, |text| println!("selection: {text}"));
assert_eq!(processed, 1);

struct StopImmediately;
impl CancelSignal for StopImmediately {
    fn is_cancelled(&self) -> bool { true }
}

let mac_monitor = CaptureMonitor::new(MacOSSelectionMonitor::default());
let _native_pref = MacOSSelectionMonitor::new_with_options(MacOSSelectionMonitorOptions {
    poll_interval: std::time::Duration::from_millis(120),
    backend: MacOSMonitorBackend::NativeObserverPreferred,
    native_queue_capacity: 256,
    native_event_pump: None, // defaults to AxObserverBridge drain pump when native observer activates
    active_pid_provider: None, // optional test/integration hook; defaults to active window PID
});
let _ = _native_pref.enqueue_native_selection_event("hello from observer");
let _ = _native_pref.ingest_native_observer_payload(
    MacOSNativeEventSource::AXObserverSelectionChanged,
    "hello from axobserver callback",
);
let cancel = StopImmediately;
let _processed = mac_monitor.poll_until_cancelled(
    std::time::Duration::from_millis(120),
    &cancel, // replace with your own cancellation signal
    |text| println!("live selection: {text}"),
);

let guard = MonitorSpamGuard {
    suppress_identical: true,
    min_emit_interval: std::time::Duration::ZERO,
    min_emit_interval_same_text: std::time::Duration::from_millis(200),
    normalize_whitespace: true,
    stable_polls_required: 2,
};
let _guarded = mac_monitor.poll_until_cancelled_guarded(
    std::time::Duration::from_millis(120),
    &cancel,
    &guard,
    |text| println!("de-spammed selection: {text}"),
);
let _native_stats = _native_pref.native_observer_stats();
let _stats = mac_monitor.poll_until_cancelled_guarded_with_stats(
    std::time::Duration::from_millis(120),
    &cancel,
    &guard,
    |_text| {},
);

let mut metrics = CaptureMetrics::default();
// metrics.record_outcome(&capture_outcome);

Current limitations:

  • Native callback integration is fully wired on macOS; Windows/Linux expose native-event-preferred queue-and-pump scaffolds with polling fallback
  • macOS NativeObserverPreferred uses AXObserver callback ingress with a native-event queue and safe polling fallback
  • No async stream integration exists yet
  • None from backend is treated as "no more events" by run(...) APIs
  • For anti-spam behavior, prefer poll_until_cancelled_guarded(...) with MonitorSpamGuard

Return Types

  • CaptureOutcome::{Success, Failure} - Result of capture attempt
  • CaptureRichOutcome::{Success, Failure} - Rich result with plain fallback semantics
  • CaptureStatus - Detailed status codes for deterministic UX mapping
  • FailureKind - Categorization of failure modes
  • CaptureTrace - Complete trace of all attempts made

Rich Content Capture (rich-content feature)

capture_rich(...) preserves backward compatibility by keeping the plain capture engine as baseline, then optionally attaching clipboard rich payloads.

  • Rich payload sources (current):
    • macOS direct AX RTF (AXRTFForRange) when capture method is accessibility-based
    • Windows direct UIA text wrapped to minimal RTF when capture method is accessibility-based (windows-beta)
    • Linux direct AT-SPI text wrapped to minimal RTF when capture method is accessibility-based (linux-alpha)
    • Clipboard HTML/RTF fallback
  • Guardrail: rich payload is accepted only when clipboard plain text matches captured plain text
  • Fallback: if guard fails (or payload unavailable/oversized), result degrades to plain content

CaptureRichOptions::allow_direct_accessibility_rich controls the direct AX path and defaults to true. CaptureRichOptions::conversion = Some(RichConversion::Markdown) enables markdown normalization and populates RichPayload.markdown (powered by quick_html2md and rtf-to-html).

#[cfg(feature = "rich-content")]
{
    use selection_capture::{capture_rich, CaptureRichOptions, CaptureRichOutcome};

    let rich_options = CaptureRichOptions::default();
    match capture_rich(&platform, &store, &cancel, &adapters, &rich_options) {
        CaptureRichOutcome::Success(ok) => println!("Captured: {:?}", ok.content),
        CaptureRichOutcome::Failure(err) => eprintln!("Capture failed: {:?}", err.status),
    }
}

Example with Custom Options

use selection_capture::{CaptureOptions, RetryPolicy};
use std::time::Duration;

let options = CaptureOptions {
    retry_policy: RetryPolicy {
        primary_accessibility: vec![Duration::from_millis(0), Duration::from_millis(80)],
        range_accessibility: vec![Duration::from_millis(0)],
        clipboard: vec![Duration::from_millis(120)],
        poll_interval: Duration::from_millis(20),
    },
    interleave_method_retries: true,
    collect_trace: true,
    allow_clipboard_borrow: true,
    overall_timeout: Duration::from_millis(500),
    strategy_override: None,
};

Permission Notes (macOS)

Depending on the target application and capture strategy, users may need to grant:

  1. Accessibility Permission
    System Settings โ†’ Privacy & Security โ†’ Accessibility

  2. Automation Permission
    Required for AppleScript fallback in some application combinations

The library will gracefully degrade through available strategies based on permissions.

Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  capture()      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”
    โ”‚ Strategy โ”‚
    โ”‚ Manager  โ”‚
    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
         โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚ 1. Accessibility API โ”‚ โ† Primary method
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    โ”‚ 2. AppleScript       โ”‚ โ† Fallback 1
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    โ”‚ 3. Clipboard Monitor โ”‚ โ† Fallback 2
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Each strategy is attempted in order, with automatic retry and detailed tracing.

Contributing

Contributions are welcome! Please see our Contributing Guide for details on:

  • Reporting bugs
  • Suggesting features
  • Submitting pull requests
  • Code style and testing

Development Setup

# Clone the repository
git clone https://github.com/maemreyo/selection-capture.git
cd selection-capture

# Build
cargo build

# Run tests
cargo test

# Run the Windows beta smoke path on any host
cargo test --features windows-beta --lib
cargo test --features windows-beta --test windows_smoke

# Run the async wrapper tests
cargo test --features async --lib
cargo test --features async --test async_capture

# Check formatting
cargo fmt --check

# Run linter
cargo clippy --all-targets

Documentation

Related Projects

This library was extracted from the zmr-koe project, which provides a complete text capture solution.

License

MIT License - See LICENSE file for details.

Acknowledgments

Thanks to all contributors and the Rust community for making this possible!


Built with โค๏ธ by zamery and contributors