Skip to main content

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