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}