use crate::profile::TriState;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActiveApp {
pub bundle_id: String,
pub name: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CaptureMethod {
AxSelectedText,
AxSelectedTextRange,
ClipboardBorrowAppleScript,
ClipboardBorrowCgEvent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CaptureStatus {
EmptySelection,
PermissionDenied,
AppBlocked,
ClipboardBorrowAmbiguous,
StrategyExhausted,
TimedOut,
Cancelled,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FailureKind {
PermissionDenied,
AppBlocked,
EmptySelection,
ClipboardAmbiguous,
TimedOut,
Cancelled,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CleanupStatus {
Clean,
ClipboardRestoreFailed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UserHint {
GrantAccessibilityPermission,
GrantAutomationPermission,
TryManualCopy,
AppBlocksDirectCapture,
RetryInFocusedApp,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RetryPolicy {
pub ax_text: Vec<Duration>,
pub ax_range: Vec<Duration>,
pub clipboard_borrow: Vec<Duration>,
pub poll_interval: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
ax_text: vec![Duration::from_millis(0), Duration::from_millis(60)],
ax_range: vec![Duration::from_millis(0)],
clipboard_borrow: vec![Duration::from_millis(120), Duration::from_millis(220)],
poll_interval: Duration::from_millis(20),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureOptions {
pub allow_clipboard_borrow: bool,
pub retry_policy: RetryPolicy,
pub collect_trace: bool,
pub overall_timeout: Duration,
pub strategy_override: Option<Vec<CaptureMethod>>,
}
impl Default for CaptureOptions {
fn default() -> Self {
Self {
allow_clipboard_borrow: true,
retry_policy: RetryPolicy::default(),
collect_trace: false,
overall_timeout: Duration::from_millis(500),
strategy_override: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TraceEvent {
CaptureStarted,
ActiveAppDetected(ActiveApp),
MethodStarted(CaptureMethod),
MethodSucceeded(CaptureMethod),
MethodReturnedEmpty(CaptureMethod),
MethodFailed {
method: CaptureMethod,
kind: FailureKind,
},
RetryWaitStarted {
method: CaptureMethod,
delay: Duration,
},
RetryWaitSkipped {
method: CaptureMethod,
remaining_budget: Duration,
needed_delay: Duration,
},
Cancelled,
TimedOut,
CleanupFinished(CleanupStatus),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureTrace {
pub events: Vec<TraceEvent>,
pub cleanup_status: CleanupStatus,
}
impl Default for CaptureTrace {
fn default() -> Self {
Self {
events: Vec::new(),
cleanup_status: CleanupStatus::Clean,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureSuccess {
pub text: String,
pub method: CaptureMethod,
pub trace: Option<CaptureTrace>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureFailureContext {
pub status: CaptureStatus,
pub active_app: Option<ActiveApp>,
pub methods_tried: Vec<CaptureMethod>,
pub last_method: Option<CaptureMethod>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureFailure {
pub status: CaptureStatus,
pub hint: Option<UserHint>,
pub trace: Option<CaptureTrace>,
pub cleanup_failed: bool,
pub context: CaptureFailureContext,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CaptureOutcome {
Success(CaptureSuccess),
Failure(CaptureFailure),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PlatformAttemptResult {
Success(String),
EmptySelection,
PermissionDenied,
AppBlocked,
ClipboardBorrowAmbiguous,
Unavailable,
}
impl PlatformAttemptResult {
pub fn failure_kind(self) -> Option<FailureKind> {
match self {
Self::EmptySelection => Some(FailureKind::EmptySelection),
Self::PermissionDenied => Some(FailureKind::PermissionDenied),
Self::AppBlocked => Some(FailureKind::AppBlocked),
Self::ClipboardBorrowAmbiguous => Some(FailureKind::ClipboardAmbiguous),
Self::Unavailable | Self::Success(_) => None,
}
}
}
impl CaptureMethod {
pub fn is_ax(self) -> bool {
matches!(self, Self::AxSelectedText | Self::AxSelectedTextRange)
}
pub fn is_clipboard(self) -> bool {
matches!(
self,
Self::ClipboardBorrowAppleScript | Self::ClipboardBorrowCgEvent
)
}
pub fn retry_delays(self, policy: &RetryPolicy) -> &[Duration] {
match self {
Self::AxSelectedText => &policy.ax_text,
Self::AxSelectedTextRange => &policy.ax_range,
Self::ClipboardBorrowAppleScript | Self::ClipboardBorrowCgEvent => {
&policy.clipboard_borrow
}
}
}
}
pub fn default_method_order(allow_clipboard_borrow: bool) -> Vec<CaptureMethod> {
let mut methods = vec![
CaptureMethod::AxSelectedText,
CaptureMethod::AxSelectedTextRange,
];
if allow_clipboard_borrow {
methods.push(CaptureMethod::ClipboardBorrowAppleScript);
}
methods
}
pub fn status_from_failure_kind(kind: FailureKind) -> CaptureStatus {
match kind {
FailureKind::PermissionDenied => CaptureStatus::PermissionDenied,
FailureKind::AppBlocked => CaptureStatus::AppBlocked,
FailureKind::EmptySelection => CaptureStatus::EmptySelection,
FailureKind::ClipboardAmbiguous => CaptureStatus::ClipboardBorrowAmbiguous,
FailureKind::TimedOut => CaptureStatus::TimedOut,
FailureKind::Cancelled => CaptureStatus::Cancelled,
}
}
pub fn update_for_method_result(
method: CaptureMethod,
result: &PlatformAttemptResult,
) -> crate::profile::AppProfileUpdate {
let mut update = crate::profile::AppProfileUpdate::default();
if method.is_ax() {
update.ax_supported = match result {
PlatformAttemptResult::Success(_) => Some(TriState::Yes),
PlatformAttemptResult::PermissionDenied | PlatformAttemptResult::AppBlocked => {
Some(TriState::No)
}
_ => None,
};
}
if method.is_clipboard() {
update.clipboard_borrow_supported = match result {
PlatformAttemptResult::Success(_) => Some(TriState::Yes),
PlatformAttemptResult::PermissionDenied | PlatformAttemptResult::AppBlocked => {
Some(TriState::No)
}
_ => None,
};
}
if let PlatformAttemptResult::Success(_) = result {
update.last_success_method = Some(method);
} else if let Some(kind) = result.clone().failure_kind() {
update.last_failure_kind = Some(kind);
}
update
}