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}