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}