Skip to main content

selection_capture/
types.rs

1use crate::profile::TriState;
2use std::time::Duration;
3
4#[cfg(target_os = "macos")]
5pub use core_graphics_types::geometry::{CGPoint, CGRect, CGSize};
6
7#[cfg(not(target_os = "macos"))]
8#[derive(Clone, Copy, Debug, Default, PartialEq)]
9pub struct CGPoint {
10    pub x: f64,
11    pub y: f64,
12}
13
14#[cfg(not(target_os = "macos"))]
15#[derive(Clone, Copy, Debug, Default, PartialEq)]
16pub struct CGSize {
17    pub width: f64,
18    pub height: f64,
19}
20
21#[cfg(not(target_os = "macos"))]
22#[derive(Clone, Copy, Debug, Default, PartialEq)]
23pub struct CGRect {
24    pub origin: CGPoint,
25    pub size: CGSize,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct ActiveApp {
30    pub bundle_id: String,
31    pub name: String,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum CaptureMethod {
36    AccessibilityPrimary,
37    AccessibilityRange,
38    ClipboardBorrow,
39    SyntheticCopy,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum CaptureStatus {
44    EmptySelection,
45    PermissionDenied,
46    AppBlocked,
47    ClipboardBorrowAmbiguous,
48    StrategyExhausted,
49    TimedOut,
50    Cancelled,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum FailureKind {
55    PermissionDenied,
56    AppBlocked,
57    EmptySelection,
58    ClipboardAmbiguous,
59    TimedOut,
60    Cancelled,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum CleanupStatus {
65    Clean,
66    ClipboardRestoreFailed,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum UserHint {
71    GrantAccessibilityPermission,
72    GrantAutomationPermission,
73    TryManualCopy,
74    AppBlocksDirectCapture,
75    RetryInFocusedApp,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct RetryPolicy {
80    pub primary_accessibility: Vec<Duration>,
81    pub range_accessibility: Vec<Duration>,
82    pub clipboard: Vec<Duration>,
83    pub poll_interval: Duration,
84}
85
86impl Default for RetryPolicy {
87    fn default() -> Self {
88        Self {
89            primary_accessibility: vec![Duration::from_millis(0), Duration::from_millis(60)],
90            range_accessibility: vec![Duration::from_millis(0)],
91            clipboard: vec![Duration::from_millis(120), Duration::from_millis(220)],
92            poll_interval: Duration::from_millis(20),
93        }
94    }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct CaptureOptions {
99    pub allow_clipboard_borrow: bool,
100    pub retry_policy: RetryPolicy,
101    pub interleave_method_retries: bool,
102    pub collect_trace: bool,
103    pub overall_timeout: Duration,
104    pub strategy_override: Option<Vec<CaptureMethod>>,
105}
106
107impl Default for CaptureOptions {
108    fn default() -> Self {
109        Self {
110            allow_clipboard_borrow: true,
111            retry_policy: RetryPolicy::default(),
112            interleave_method_retries: true,
113            collect_trace: false,
114            overall_timeout: Duration::from_millis(500),
115            strategy_override: None,
116        }
117    }
118}
119
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub enum TraceEvent {
122    CaptureStarted,
123    ActiveAppDetected(ActiveApp),
124    MethodStarted(CaptureMethod),
125    MethodFinished {
126        method: CaptureMethod,
127        elapsed: Duration,
128    },
129    MethodSucceeded(CaptureMethod),
130    MethodReturnedEmpty(CaptureMethod),
131    MethodFailed {
132        method: CaptureMethod,
133        kind: FailureKind,
134    },
135    RetryWaitStarted {
136        method: CaptureMethod,
137        delay: Duration,
138    },
139    RetryWaitSkipped {
140        method: CaptureMethod,
141        remaining_budget: Duration,
142        needed_delay: Duration,
143    },
144    Cancelled,
145    TimedOut,
146    CleanupFinished(CleanupStatus),
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct CaptureTrace {
151    pub events: Vec<TraceEvent>,
152    pub cleanup_status: CleanupStatus,
153    pub total_elapsed: Duration,
154}
155
156impl Default for CaptureTrace {
157    fn default() -> Self {
158        Self {
159            events: Vec::new(),
160            cleanup_status: CleanupStatus::Clean,
161            total_elapsed: Duration::ZERO,
162        }
163    }
164}
165
166#[derive(Clone, Debug)]
167pub struct CaptureSuccess {
168    pub text: String,
169    pub method: CaptureMethod,
170    pub focused_window_frame: Option<CGRect>,
171    pub trace: Option<CaptureTrace>,
172}
173
174impl PartialEq for CaptureSuccess {
175    fn eq(&self, other: &Self) -> bool {
176        self.text == other.text
177            && self.method == other.method
178            && self.trace == other.trace
179            && rect_option_eq(self.focused_window_frame, other.focused_window_frame)
180    }
181}
182
183fn rect_option_eq(left: Option<CGRect>, right: Option<CGRect>) -> bool {
184    match (left, right) {
185        (Some(a), Some(b)) => {
186            a.origin.x.to_bits() == b.origin.x.to_bits()
187                && a.origin.y.to_bits() == b.origin.y.to_bits()
188                && a.size.width.to_bits() == b.size.width.to_bits()
189                && a.size.height.to_bits() == b.size.height.to_bits()
190        }
191        (None, None) => true,
192        _ => false,
193    }
194}
195
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub struct CaptureFailureContext {
198    pub status: CaptureStatus,
199    pub active_app: Option<ActiveApp>,
200    pub methods_tried: Vec<CaptureMethod>,
201    pub last_method: Option<CaptureMethod>,
202}
203
204#[derive(Clone, Debug, PartialEq, Eq)]
205pub struct CaptureFailure {
206    pub status: CaptureStatus,
207    pub hint: Option<UserHint>,
208    pub trace: Option<CaptureTrace>,
209    pub cleanup_failed: bool,
210    pub context: CaptureFailureContext,
211}
212
213#[derive(Clone, Debug, PartialEq)]
214pub enum CaptureOutcome {
215    Success(CaptureSuccess),
216    Failure(CaptureFailure),
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
220pub struct WouldBlock;
221
222#[derive(Clone, Debug, PartialEq, Eq)]
223pub enum PlatformAttemptResult {
224    Success(String),
225    EmptySelection,
226    PermissionDenied,
227    AppBlocked,
228    ClipboardBorrowAmbiguous,
229    Unavailable,
230}
231
232impl PlatformAttemptResult {
233    pub fn failure_kind(self) -> Option<FailureKind> {
234        match self {
235            Self::EmptySelection => Some(FailureKind::EmptySelection),
236            Self::PermissionDenied => Some(FailureKind::PermissionDenied),
237            Self::AppBlocked => Some(FailureKind::AppBlocked),
238            Self::ClipboardBorrowAmbiguous => Some(FailureKind::ClipboardAmbiguous),
239            Self::Unavailable | Self::Success(_) => None,
240        }
241    }
242}
243
244impl CaptureMethod {
245    pub fn is_ax(self) -> bool {
246        matches!(self, Self::AccessibilityPrimary | Self::AccessibilityRange)
247    }
248
249    pub fn is_clipboard(self) -> bool {
250        matches!(self, Self::ClipboardBorrow | Self::SyntheticCopy)
251    }
252
253    pub fn retry_delays(self, policy: &RetryPolicy) -> &[Duration] {
254        match self {
255            Self::AccessibilityPrimary => &policy.primary_accessibility,
256            Self::AccessibilityRange => &policy.range_accessibility,
257            Self::ClipboardBorrow | Self::SyntheticCopy => &policy.clipboard,
258        }
259    }
260}
261
262pub fn default_method_order(allow_clipboard_borrow: bool) -> Vec<CaptureMethod> {
263    let mut methods = vec![
264        CaptureMethod::AccessibilityPrimary,
265        CaptureMethod::AccessibilityRange,
266    ];
267    if allow_clipboard_borrow {
268        methods.push(CaptureMethod::ClipboardBorrow);
269    }
270    methods
271}
272
273pub fn status_from_failure_kind(kind: FailureKind) -> CaptureStatus {
274    match kind {
275        FailureKind::PermissionDenied => CaptureStatus::PermissionDenied,
276        FailureKind::AppBlocked => CaptureStatus::AppBlocked,
277        FailureKind::EmptySelection => CaptureStatus::EmptySelection,
278        FailureKind::ClipboardAmbiguous => CaptureStatus::ClipboardBorrowAmbiguous,
279        FailureKind::TimedOut => CaptureStatus::TimedOut,
280        FailureKind::Cancelled => CaptureStatus::Cancelled,
281    }
282}
283
284pub fn update_for_method_result(
285    method: CaptureMethod,
286    result: &PlatformAttemptResult,
287) -> crate::profile::AppProfileUpdate {
288    let mut update = crate::profile::AppProfileUpdate::default();
289    if method.is_ax() {
290        update.ax_supported = match result {
291            PlatformAttemptResult::Success(_) => Some(TriState::Yes),
292            PlatformAttemptResult::PermissionDenied | PlatformAttemptResult::AppBlocked => {
293                Some(TriState::No)
294            }
295            _ => None,
296        };
297    }
298    if method.is_clipboard() {
299        update.clipboard_borrow_supported = match result {
300            PlatformAttemptResult::Success(_) => Some(TriState::Yes),
301            PlatformAttemptResult::PermissionDenied | PlatformAttemptResult::AppBlocked => {
302                Some(TriState::No)
303            }
304            _ => None,
305        };
306    }
307    if let PlatformAttemptResult::Success(_) = result {
308        update.last_success_method = Some(method);
309    } else if let Some(kind) = result.clone().failure_kind() {
310        update.last_failure_kind = Some(kind);
311    }
312    update
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn default_method_order_includes_clipboard_when_allowed() {
321        assert_eq!(
322            default_method_order(true),
323            vec![
324                CaptureMethod::AccessibilityPrimary,
325                CaptureMethod::AccessibilityRange,
326                CaptureMethod::ClipboardBorrow,
327            ]
328        );
329    }
330
331    #[test]
332    fn default_method_order_excludes_clipboard_when_disallowed() {
333        assert_eq!(
334            default_method_order(false),
335            vec![
336                CaptureMethod::AccessibilityPrimary,
337                CaptureMethod::AccessibilityRange,
338            ]
339        );
340    }
341
342    #[test]
343    fn retry_delays_use_platform_neutral_policy_fields() {
344        let policy = RetryPolicy {
345            primary_accessibility: vec![Duration::from_millis(1)],
346            range_accessibility: vec![Duration::from_millis(2)],
347            clipboard: vec![Duration::from_millis(3)],
348            poll_interval: Duration::from_millis(4),
349        };
350
351        assert_eq!(
352            CaptureMethod::AccessibilityPrimary.retry_delays(&policy),
353            &[Duration::from_millis(1)]
354        );
355        assert_eq!(
356            CaptureMethod::AccessibilityRange.retry_delays(&policy),
357            &[Duration::from_millis(2)]
358        );
359        assert_eq!(
360            CaptureMethod::ClipboardBorrow.retry_delays(&policy),
361            &[Duration::from_millis(3)]
362        );
363        assert_eq!(
364            CaptureMethod::SyntheticCopy.retry_delays(&policy),
365            &[Duration::from_millis(3)]
366        );
367    }
368}