use std::error::Error as StdError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CaptureError {
#[error(
"this compositor cannot capture individual windows (needs wlroots >= 0.20 / Sway >= 1.12)"
)]
WindowsUnsupported,
#[error("no outputs available")]
NoOutputs,
#[error("empty region")]
EmptyRegion,
#[error("region covers no output")]
RegionOffscreen,
#[error("output '{0}' not found")]
OutputNotFound(String),
#[error("window '{0}' not found")]
WindowNotFound(String),
#[error("no window matches {0}")]
NoWindowMatches(String),
#[error("{selector} matches {} windows:\n{}", .candidates.len(), .candidates.join("\n"))]
AmbiguousWindow {
selector: String,
candidates: Vec<String>,
},
#[error("invalid geometry '{0}' (expected 'X,Y WxH')")]
InvalidGeometry(String),
#[error("capture timed out")]
CaptureTimeout,
#[cfg(feature = "video")]
#[error("no H.264 encoder available (need NVENC, VAAPI or libx264)")]
NoVideoEncoder,
#[cfg(feature = "video")]
#[error("source too small to encode ({w}x{h})")]
SourceTooSmall {
w: u32,
h: u32,
},
#[cfg(feature = "audio")]
#[error("no audio backend available")]
NoAudioBackend,
#[error("{context}")]
Backend {
context: String,
#[source]
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
},
}
pub type Result<T, E = CaptureError> = std::result::Result<T, E>;
impl CaptureError {
pub fn msg(context: impl Into<String>) -> Self {
CaptureError::Backend {
context: context.into(),
source: None,
}
}
}
pub trait Context<T> {
fn context(self, context: impl Into<String>) -> Result<T, CaptureError>;
fn with_context<S: Into<String>>(self, f: impl FnOnce() -> S) -> Result<T, CaptureError>;
}
impl<T, E: StdError + Send + Sync + 'static> Context<T> for Result<T, E> {
fn context(self, context: impl Into<String>) -> Result<T, CaptureError> {
self.map_err(|e| CaptureError::Backend {
context: context.into(),
source: Some(Box::new(e)),
})
}
fn with_context<S: Into<String>>(self, f: impl FnOnce() -> S) -> Result<T, CaptureError> {
self.map_err(|e| CaptureError::Backend {
context: f().into(),
source: Some(Box::new(e)),
})
}
}
impl<T> Context<T> for Option<T> {
fn context(self, context: impl Into<String>) -> Result<T, CaptureError> {
self.ok_or_else(|| CaptureError::msg(context))
}
fn with_context<S: Into<String>>(self, f: impl FnOnce() -> S) -> Result<T, CaptureError> {
self.ok_or_else(|| CaptureError::msg(f()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn source(e: &CaptureError) -> Option<&(dyn StdError + 'static)> {
StdError::source(e)
}
#[test]
fn variant_display_text_is_stable() {
assert_eq!(
CaptureError::OutputNotFound("HDMI-1".into()).to_string(),
"output 'HDMI-1' not found"
);
assert_eq!(
CaptureError::WindowNotFound("42".into()).to_string(),
"window '42' not found"
);
assert_eq!(
CaptureError::InvalidGeometry("bad".into()).to_string(),
"invalid geometry 'bad' (expected 'X,Y WxH')"
);
assert!(
CaptureError::WindowsUnsupported
.to_string()
.contains("wlroots >= 0.20")
);
}
#[test]
fn ambiguous_window_lists_every_candidate() {
let e = CaptureError::AmbiguousWindow {
selector: "app-id 'firefox'".into(),
candidates: vec![" 1 firefox Inbox".into(), " 2 firefox Docs".into()],
};
assert_eq!(
e.to_string(),
"app-id 'firefox' matches 2 windows:\n 1 firefox Inbox\n 2 firefox Docs"
);
}
#[cfg(feature = "video")]
#[test]
fn source_too_small_interpolates_dimensions() {
assert_eq!(
CaptureError::SourceTooSmall { w: 4, h: 2 }.to_string(),
"source too small to encode (4x2)"
);
}
#[test]
fn msg_is_a_backend_error_without_a_cause() {
let e = CaptureError::msg("boom");
assert_eq!(e.to_string(), "boom");
assert!(source(&e).is_none());
}
#[test]
fn context_on_result_wraps_and_preserves_the_cause() {
let e: CaptureError = "x".parse::<i32>().context("parsing width").unwrap_err();
assert_eq!(e.to_string(), "parsing width");
let cause = source(&e).expect("cause preserved");
assert!(cause.to_string().contains("invalid digit"));
}
#[test]
fn with_context_is_lazy_and_only_builds_on_error() {
let ok: Result<i32> = "1"
.parse::<i32>()
.with_context(|| -> String { unreachable!("not called on Ok") });
assert_eq!(ok.unwrap(), 1);
let e = "x"
.parse::<i32>()
.with_context(|| format!("ctx {}", 7))
.unwrap_err();
assert_eq!(e.to_string(), "ctx 7");
}
#[test]
fn context_on_option_maps_none_and_passes_some_through() {
let e = None::<i32>.context("missing thing").unwrap_err();
assert_eq!(e.to_string(), "missing thing");
assert!(source(&e).is_none());
assert_eq!(Some(5).context("unused").unwrap(), 5);
}
}