screencapturekit/utils/
error.rs

1//! Error types for `ScreenCaptureKit`
2//!
3//! This module provides comprehensive error types for all operations in the library.
4//! All operations return [`SCResult<T>`] which is an alias for `Result<T, SCError>`.
5//!
6//! # Examples
7//!
8//! ```
9//! use screencapturekit::prelude::*;
10//!
11//! fn setup_capture() -> SCResult<()> {
12//!     // Configure with builder pattern
13//!     let config = SCStreamConfiguration::new()
14//!         .with_width(1920)
15//!         .with_height(1080);
16//!     Ok(())
17//! }
18//!
19//! // Pattern matching on errors
20//! match setup_capture() {
21//!     Ok(_) => println!("Success!"),
22//!     Err(SCError::InvalidDimension { field, value }) => {
23//!         eprintln!("Invalid {}: {}", field, value);
24//!     }
25//!     Err(e) => eprintln!("Error: {}", e),
26//! }
27//! ```
28
29use std::fmt;
30
31/// Result type alias for `ScreenCaptureKit` operations
32///
33/// This is a convenience alias for `Result<T, SCError>` used throughout the library.
34///
35/// # Examples
36///
37/// ```
38/// use screencapturekit::prelude::*;
39///
40/// fn validate_dimensions(width: u32, height: u32) -> SCResult<()> {
41///     if width == 0 {
42///         return Err(SCError::invalid_dimension("width", 0));
43///     }
44///     if height == 0 {
45///         return Err(SCError::invalid_dimension("height", 0));
46///     }
47///     Ok(())
48/// }
49///
50/// assert!(validate_dimensions(0, 1080).is_err());
51/// assert!(validate_dimensions(1920, 1080).is_ok());
52/// ```
53pub type SCResult<T> = Result<T, SCError>;
54
55/// Comprehensive error type for `ScreenCaptureKit` operations
56///
57/// This enum covers all possible error conditions that can occur when using
58/// the `ScreenCaptureKit` API. Each variant provides specific context about
59/// what went wrong.
60///
61/// # Examples
62///
63/// ## Creating Errors
64///
65/// ```
66/// use screencapturekit::error::SCError;
67///
68/// // Using helper methods (recommended)
69/// let err = SCError::invalid_dimension("width", 0);
70/// assert_eq!(err.to_string(), "Invalid dimension: width must be greater than 0 (got 0)");
71///
72/// let err = SCError::permission_denied("Screen Recording");
73/// assert!(err.to_string().contains("Screen Recording"));
74/// ```
75///
76/// ## Pattern Matching
77///
78/// ```
79/// use screencapturekit::error::SCError;
80///
81/// fn handle_error(err: SCError) {
82///     match err {
83///         SCError::InvalidDimension { field, value } => {
84///             println!("Invalid {}: {}", field, value);
85///         }
86///         SCError::PermissionDenied(msg) => {
87///             println!("Permission needed: {}", msg);
88///         }
89///         _ => println!("Other error: {}", err),
90///     }
91/// }
92/// ```
93#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum SCError {
96    /// Invalid configuration parameter
97    InvalidConfiguration(String),
98
99    /// Invalid dimension value (width or height)
100    InvalidDimension { field: String, value: usize },
101
102    /// Invalid pixel format
103    InvalidPixelFormat(String),
104
105    /// No shareable content available
106    NoShareableContent(String),
107
108    /// Display not found
109    DisplayNotFound(String),
110
111    /// Window not found
112    WindowNotFound(String),
113
114    /// Application not found
115    ApplicationNotFound(String),
116
117    /// Stream operation error (generic)
118    StreamError(String),
119
120    /// Failed to start capture
121    CaptureStartFailed(String),
122
123    /// Failed to stop capture
124    CaptureStopFailed(String),
125
126    /// Buffer lock error
127    BufferLockError(String),
128
129    /// Buffer unlock error
130    BufferUnlockError(String),
131
132    /// Invalid buffer
133    InvalidBuffer(String),
134
135    /// Screenshot capture error
136    ScreenshotError(String),
137
138    /// Permission denied
139    PermissionDenied(String),
140
141    /// Feature not available on this macOS version
142    FeatureNotAvailable {
143        feature: String,
144        required_version: String,
145    },
146
147    /// FFI error
148    FFIError(String),
149
150    /// Null pointer encountered
151    NullPointer(String),
152
153    /// Timeout error
154    Timeout(String),
155
156    /// Generic internal error
157    InternalError(String),
158
159    /// OS error with code (for non-SCStream errors)
160    OSError { code: i32, message: String },
161
162    /// `ScreenCaptureKit` stream error with specific error code
163    ///
164    /// This variant wraps Apple's `SCStreamError.Code` for precise error handling.
165    /// Use [`SCStreamErrorCode`] to match specific error conditions.
166    SCStreamError {
167        code: SCStreamErrorCode,
168        message: Option<String>,
169    },
170}
171
172impl fmt::Display for SCError {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        match self {
175            Self::InvalidConfiguration(msg) => write!(f, "Invalid configuration: {msg}"),
176            Self::InvalidDimension { field, value } => {
177                write!(
178                    f,
179                    "Invalid dimension: {field} must be greater than 0 (got {value})"
180                )
181            }
182            Self::InvalidPixelFormat(msg) => write!(f, "Invalid pixel format: {msg}"),
183            Self::NoShareableContent(msg) => write!(f, "No shareable content available: {msg}"),
184            Self::DisplayNotFound(msg) => write!(f, "Display not found: {msg}"),
185            Self::WindowNotFound(msg) => write!(f, "Window not found: {msg}"),
186            Self::ApplicationNotFound(msg) => write!(f, "Application not found: {msg}"),
187            Self::StreamError(msg) => write!(f, "Stream error: {msg}"),
188            Self::CaptureStartFailed(msg) => write!(f, "Failed to start capture: {msg}"),
189            Self::CaptureStopFailed(msg) => write!(f, "Failed to stop capture: {msg}"),
190            Self::BufferLockError(msg) => write!(f, "Failed to lock pixel buffer: {msg}"),
191            Self::BufferUnlockError(msg) => write!(f, "Failed to unlock pixel buffer: {msg}"),
192            Self::InvalidBuffer(msg) => write!(f, "Invalid buffer: {msg}"),
193            Self::ScreenshotError(msg) => write!(f, "Screenshot capture failed: {msg}"),
194            Self::PermissionDenied(msg) => {
195                write!(f, "Permission denied: {msg}. Check System Preferences → Security & Privacy → Screen Recording")
196            }
197            Self::FeatureNotAvailable {
198                feature,
199                required_version,
200            } => {
201                write!(
202                    f,
203                    "Feature not available: {feature} requires macOS {required_version}+"
204                )
205            }
206            Self::FFIError(msg) => write!(f, "FFI error: {msg}"),
207            Self::NullPointer(msg) => write!(f, "Null pointer: {msg}"),
208            Self::Timeout(msg) => write!(f, "Operation timed out: {msg}"),
209            Self::InternalError(msg) => write!(f, "Internal error: {msg}"),
210            Self::OSError { code, message } => write!(f, "OS error {code}: {message}"),
211            Self::SCStreamError { code, message } => {
212                if let Some(msg) = message {
213                    write!(f, "SCStream error ({code}): {msg}")
214                } else {
215                    write!(f, "SCStream error: {code}")
216                }
217            }
218        }
219    }
220}
221
222impl std::error::Error for SCError {}
223
224impl From<SCStreamErrorCode> for SCError {
225    fn from(code: SCStreamErrorCode) -> Self {
226        Self::from_stream_error_code(code)
227    }
228}
229
230impl SCError {
231    /// Create an invalid configuration error
232    ///
233    /// # Examples
234    ///
235    /// ```
236    /// use screencapturekit::error::SCError;
237    ///
238    /// let err = SCError::invalid_config("Queue depth must be positive");
239    /// assert!(err.to_string().contains("Queue depth"));
240    /// ```
241    pub fn invalid_config(message: impl Into<String>) -> Self {
242        Self::InvalidConfiguration(message.into())
243    }
244
245    /// Create an invalid dimension error
246    ///
247    /// Use this when width or height validation fails.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use screencapturekit::error::SCError;
253    ///
254    /// let err = SCError::invalid_dimension("width", 0);
255    /// assert_eq!(
256    ///     err.to_string(),
257    ///     "Invalid dimension: width must be greater than 0 (got 0)"
258    /// );
259    ///
260    /// let err = SCError::invalid_dimension("height", 0);
261    /// assert!(err.to_string().contains("height"));
262    /// ```
263    pub fn invalid_dimension(field: impl Into<String>, value: usize) -> Self {
264        Self::InvalidDimension {
265            field: field.into(),
266            value,
267        }
268    }
269
270    /// Create a stream error
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use screencapturekit::error::SCError;
276    ///
277    /// let err = SCError::stream_error("Failed to start");
278    /// assert!(err.to_string().contains("Stream error"));
279    /// ```
280    pub fn stream_error(message: impl Into<String>) -> Self {
281        Self::StreamError(message.into())
282    }
283
284    /// Create a permission denied error
285    ///
286    /// The error message automatically includes instructions to check System Preferences.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use screencapturekit::error::SCError;
292    ///
293    /// let err = SCError::permission_denied("Screen Recording");
294    /// let msg = err.to_string();
295    /// assert!(msg.contains("Screen Recording"));
296    /// assert!(msg.contains("System Preferences"));
297    /// ```
298    pub fn permission_denied(message: impl Into<String>) -> Self {
299        Self::PermissionDenied(message.into())
300    }
301
302    /// Create an FFI error
303    ///
304    /// Use for errors crossing the Rust/Swift boundary.
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// use screencapturekit::error::SCError;
310    ///
311    /// let err = SCError::ffi_error("Swift bridge call failed");
312    /// assert!(err.to_string().contains("FFI error"));
313    /// ```
314    pub fn ffi_error(message: impl Into<String>) -> Self {
315        Self::FFIError(message.into())
316    }
317
318    /// Create an internal error
319    ///
320    /// # Examples
321    ///
322    /// ```
323    /// use screencapturekit::error::SCError;
324    ///
325    /// let err = SCError::internal_error("Unexpected state");
326    /// assert!(err.to_string().contains("Internal error"));
327    /// ```
328    pub fn internal_error(message: impl Into<String>) -> Self {
329        Self::InternalError(message.into())
330    }
331
332    /// Create a null pointer error
333    ///
334    /// # Examples
335    ///
336    /// ```
337    /// use screencapturekit::error::SCError;
338    ///
339    /// let err = SCError::null_pointer("Display pointer");
340    /// assert!(err.to_string().contains("Null pointer"));
341    /// assert!(err.to_string().contains("Display pointer"));
342    /// ```
343    pub fn null_pointer(context: impl Into<String>) -> Self {
344        Self::NullPointer(context.into())
345    }
346
347    /// Create a feature not available error
348    ///
349    /// Use when a feature requires a newer macOS version.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// use screencapturekit::error::SCError;
355    ///
356    /// let err = SCError::feature_not_available("Screenshot Manager", "14.0");
357    /// let msg = err.to_string();
358    /// assert!(msg.contains("Screenshot Manager"));
359    /// assert!(msg.contains("14.0"));
360    /// ```
361    pub fn feature_not_available(feature: impl Into<String>, version: impl Into<String>) -> Self {
362        Self::FeatureNotAvailable {
363            feature: feature.into(),
364            required_version: version.into(),
365        }
366    }
367
368    /// Create a buffer lock error
369    ///
370    /// # Examples
371    ///
372    /// ```
373    /// use screencapturekit::error::SCError;
374    ///
375    /// let err = SCError::buffer_lock_error("Already locked");
376    /// assert!(err.to_string().contains("lock pixel buffer"));
377    /// ```
378    pub fn buffer_lock_error(message: impl Into<String>) -> Self {
379        Self::BufferLockError(message.into())
380    }
381
382    /// Create an OS error with error code
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use screencapturekit::error::SCError;
388    ///
389    /// let err = SCError::os_error(-1, "System call failed");
390    /// let msg = err.to_string();
391    /// assert!(msg.contains("-1"));
392    /// assert!(msg.contains("System call failed"));
393    /// ```
394    pub fn os_error(code: i32, message: impl Into<String>) -> Self {
395        Self::OSError {
396            code,
397            message: message.into(),
398        }
399    }
400
401    /// Create an error from an `SCStreamErrorCode`
402    ///
403    /// # Examples
404    ///
405    /// ```
406    /// use screencapturekit::error::{SCError, SCStreamErrorCode};
407    ///
408    /// let err = SCError::from_stream_error_code(SCStreamErrorCode::UserDeclined);
409    /// assert!(err.to_string().contains("User declined"));
410    /// ```
411    pub fn from_stream_error_code(code: SCStreamErrorCode) -> Self {
412        Self::SCStreamError {
413            code,
414            message: None,
415        }
416    }
417
418    /// Create an error from an `SCStreamErrorCode` with additional message
419    ///
420    /// # Examples
421    ///
422    /// ```
423    /// use screencapturekit::error::{SCError, SCStreamErrorCode};
424    ///
425    /// let err = SCError::from_stream_error_code_with_message(
426    ///     SCStreamErrorCode::FailedToStart,
427    ///     "No available displays"
428    /// );
429    /// assert!(err.to_string().contains("Failed to start"));
430    /// ```
431    pub fn from_stream_error_code_with_message(
432        code: SCStreamErrorCode,
433        message: impl Into<String>,
434    ) -> Self {
435        Self::SCStreamError {
436            code,
437            message: Some(message.into()),
438        }
439    }
440
441    /// Create an error from a raw error code
442    ///
443    /// If the code matches a known `SCStreamErrorCode`, creates an `SCStreamError`.
444    /// Otherwise, creates an `OSError`.
445    ///
446    /// # Examples
447    ///
448    /// ```
449    /// use screencapturekit::error::SCError;
450    ///
451    /// // Known SCStreamError code
452    /// let err = SCError::from_error_code(-3801); // UserDeclined
453    /// assert!(matches!(err, SCError::SCStreamError { .. }));
454    ///
455    /// // Unknown code falls back to OSError
456    /// let err = SCError::from_error_code(-999);
457    /// assert!(matches!(err, SCError::OSError { .. }));
458    /// ```
459    pub fn from_error_code(code: i32) -> Self {
460        SCStreamErrorCode::from_raw(code).map_or_else(
461            || Self::OSError {
462                code,
463                message: "Unknown error".to_string(),
464            },
465            Self::from_stream_error_code,
466        )
467    }
468
469    /// Get the `SCStreamErrorCode` if this is an `SCStreamError`
470    ///
471    /// # Examples
472    ///
473    /// ```
474    /// use screencapturekit::error::{SCError, SCStreamErrorCode};
475    ///
476    /// let err = SCError::from_stream_error_code(SCStreamErrorCode::UserDeclined);
477    /// assert_eq!(err.stream_error_code(), Some(SCStreamErrorCode::UserDeclined));
478    ///
479    /// let err = SCError::StreamError("test".to_string());
480    /// assert_eq!(err.stream_error_code(), None);
481    /// ```
482    pub fn stream_error_code(&self) -> Option<SCStreamErrorCode> {
483        match self {
484            Self::SCStreamError { code, .. } => Some(*code),
485            _ => None,
486        }
487    }
488}
489
490/// Error domain for `ScreenCaptureKit` errors
491pub const SC_STREAM_ERROR_DOMAIN: &str = "com.apple.screencapturekit";
492
493/// Error codes from Apple's `SCStreamError.Code`
494///
495/// These correspond to the error codes returned by `ScreenCaptureKit` operations.
496///
497/// Based on Apple's `SCStreamErrorCode` from `SCError.h`.
498#[repr(i32)]
499#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
500pub enum SCStreamErrorCode {
501    /// The user chose not to authorize capture
502    UserDeclined = -3801,
503    /// The stream failed to start
504    FailedToStart = -3802,
505    /// The stream failed due to missing entitlements
506    MissingEntitlements = -3803,
507    /// Failed during recording - application connection invalid
508    FailedApplicationConnectionInvalid = -3804,
509    /// Failed during recording - application connection interrupted
510    FailedApplicationConnectionInterrupted = -3805,
511    /// Failed during recording - context id does not match application
512    FailedNoMatchingApplicationContext = -3806,
513    /// Failed due to attempting to start a stream that's already in a recording state
514    AttemptToStartStreamState = -3807,
515    /// Failed due to attempting to stop a stream that's already in a recording state
516    AttemptToStopStreamState = -3808,
517    /// Failed due to attempting to update the filter on a stream
518    AttemptToUpdateFilterState = -3809,
519    /// Failed due to attempting to update stream config on a stream
520    AttemptToConfigState = -3810,
521    /// Failed to start due to video/audio capture failure
522    InternalError = -3811,
523    /// Failed due to invalid parameter
524    InvalidParameter = -3812,
525    /// Failed due to no window list
526    NoWindowList = -3813,
527    /// Failed due to no display list
528    NoDisplayList = -3814,
529    /// Failed due to no display or window list to capture
530    NoCaptureSource = -3815,
531    /// Failed to remove stream
532    RemovingStream = -3816,
533    /// The stream was stopped by the user
534    UserStopped = -3817,
535    /// The stream failed to start audio (macOS 13.0+)
536    FailedToStartAudioCapture = -3818,
537    /// The stream failed to stop audio (macOS 13.0+)
538    FailedToStopAudioCapture = -3819,
539    /// The stream failed to start microphone (macOS 15.0+)
540    FailedToStartMicrophoneCapture = -3820,
541    /// The stream was stopped by the system (macOS 15.0+)
542    SystemStoppedStream = -3821,
543}
544
545impl SCStreamErrorCode {
546    /// Create from raw error code value
547    pub fn from_raw(code: i32) -> Option<Self> {
548        match code {
549            -3801 => Some(Self::UserDeclined),
550            -3802 => Some(Self::FailedToStart),
551            -3803 => Some(Self::MissingEntitlements),
552            -3804 => Some(Self::FailedApplicationConnectionInvalid),
553            -3805 => Some(Self::FailedApplicationConnectionInterrupted),
554            -3806 => Some(Self::FailedNoMatchingApplicationContext),
555            -3807 => Some(Self::AttemptToStartStreamState),
556            -3808 => Some(Self::AttemptToStopStreamState),
557            -3809 => Some(Self::AttemptToUpdateFilterState),
558            -3810 => Some(Self::AttemptToConfigState),
559            -3811 => Some(Self::InternalError),
560            -3812 => Some(Self::InvalidParameter),
561            -3813 => Some(Self::NoWindowList),
562            -3814 => Some(Self::NoDisplayList),
563            -3815 => Some(Self::NoCaptureSource),
564            -3816 => Some(Self::RemovingStream),
565            -3817 => Some(Self::UserStopped),
566            -3818 => Some(Self::FailedToStartAudioCapture),
567            -3819 => Some(Self::FailedToStopAudioCapture),
568            -3820 => Some(Self::FailedToStartMicrophoneCapture),
569            -3821 => Some(Self::SystemStoppedStream),
570            _ => None,
571        }
572    }
573
574    /// Get the raw error code value
575    pub const fn as_raw(self) -> i32 {
576        self as i32
577    }
578}
579
580impl std::fmt::Display for SCStreamErrorCode {
581    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
582        match self {
583            Self::UserDeclined => write!(f, "User declined screen recording"),
584            Self::FailedToStart => write!(f, "Failed to start stream"),
585            Self::MissingEntitlements => write!(f, "Missing entitlements"),
586            Self::FailedApplicationConnectionInvalid => {
587                write!(f, "Application connection invalid")
588            }
589            Self::FailedApplicationConnectionInterrupted => {
590                write!(f, "Application connection interrupted")
591            }
592            Self::FailedNoMatchingApplicationContext => {
593                write!(f, "No matching application context")
594            }
595            Self::AttemptToStartStreamState => write!(f, "Stream is already running"),
596            Self::AttemptToStopStreamState => write!(f, "Stream is not running"),
597            Self::AttemptToUpdateFilterState => write!(f, "Cannot update filter while streaming"),
598            Self::AttemptToConfigState => write!(f, "Cannot configure while streaming"),
599            Self::InternalError => write!(f, "Internal error"),
600            Self::InvalidParameter => write!(f, "Invalid parameter"),
601            Self::NoWindowList => write!(f, "No window list provided"),
602            Self::NoDisplayList => write!(f, "No display list provided"),
603            Self::NoCaptureSource => write!(f, "No capture source provided"),
604            Self::RemovingStream => write!(f, "Failed to remove stream"),
605            Self::UserStopped => write!(f, "User stopped the stream"),
606            Self::FailedToStartAudioCapture => write!(f, "Failed to start audio capture"),
607            Self::FailedToStopAudioCapture => write!(f, "Failed to stop audio capture"),
608            Self::FailedToStartMicrophoneCapture => write!(f, "Failed to start microphone capture"),
609            Self::SystemStoppedStream => write!(f, "System stopped the stream"),
610        }
611    }
612}