Skip to main content

selection_capture/
types.rs

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