1use std::error::Error as StdError;
12use thiserror::Error;
13
14#[derive(Debug, Error)]
16pub enum CaptureError {
17 #[error(
20 "this compositor cannot capture individual windows (needs wlroots >= 0.20 / Sway >= 1.12)"
21 )]
22 WindowsUnsupported,
23
24 #[error("no outputs available")]
26 NoOutputs,
27
28 #[error("empty region")]
30 EmptyRegion,
31
32 #[error("region covers no output")]
34 RegionOffscreen,
35
36 #[error("output '{0}' not found")]
38 OutputNotFound(String),
39
40 #[error("window '{0}' not found")]
42 WindowNotFound(String),
43
44 #[error("no window matches {0}")]
46 NoWindowMatches(String),
47
48 #[error("{selector} matches {} windows:\n{}", .candidates.len(), .candidates.join("\n"))]
51 AmbiguousWindow {
52 selector: String,
54 candidates: Vec<String>,
56 },
57
58 #[error("invalid geometry '{0}' (expected 'X,Y WxH')")]
60 InvalidGeometry(String),
61
62 #[error("capture timed out")]
64 CaptureTimeout,
65
66 #[cfg(feature = "video")]
68 #[error("no H.264 encoder available (need NVENC, VAAPI or libx264)")]
69 NoVideoEncoder,
70
71 #[cfg(feature = "video")]
73 #[error("source too small to encode ({w}x{h})")]
74 SourceTooSmall {
75 w: u32,
77 h: u32,
79 },
80
81 #[cfg(feature = "audio")]
83 #[error("no audio backend available")]
84 NoAudioBackend,
85
86 #[error("{context}")]
89 Backend {
90 context: String,
92 #[source]
94 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
95 },
96}
97
98pub type Result<T, E = CaptureError> = std::result::Result<T, E>;
101
102impl CaptureError {
103 pub fn msg(context: impl Into<String>) -> Self {
106 CaptureError::Backend {
107 context: context.into(),
108 source: None,
109 }
110 }
111}
112
113pub trait Context<T> {
117 fn context(self, context: impl Into<String>) -> Result<T, CaptureError>;
119 fn with_context<S: Into<String>>(self, f: impl FnOnce() -> S) -> Result<T, CaptureError>;
121}
122
123impl<T, E: StdError + Send + Sync + 'static> Context<T> for Result<T, E> {
124 fn context(self, context: impl Into<String>) -> Result<T, CaptureError> {
125 self.map_err(|e| CaptureError::Backend {
126 context: context.into(),
127 source: Some(Box::new(e)),
128 })
129 }
130 fn with_context<S: Into<String>>(self, f: impl FnOnce() -> S) -> Result<T, CaptureError> {
131 self.map_err(|e| CaptureError::Backend {
132 context: f().into(),
133 source: Some(Box::new(e)),
134 })
135 }
136}
137
138impl<T> Context<T> for Option<T> {
139 fn context(self, context: impl Into<String>) -> Result<T, CaptureError> {
140 self.ok_or_else(|| CaptureError::msg(context))
141 }
142 fn with_context<S: Into<String>>(self, f: impl FnOnce() -> S) -> Result<T, CaptureError> {
143 self.ok_or_else(|| CaptureError::msg(f()))
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn source(e: &CaptureError) -> Option<&(dyn StdError + 'static)> {
152 StdError::source(e)
153 }
154
155 #[test]
156 fn variant_display_text_is_stable() {
157 assert_eq!(
158 CaptureError::OutputNotFound("HDMI-1".into()).to_string(),
159 "output 'HDMI-1' not found"
160 );
161 assert_eq!(
162 CaptureError::WindowNotFound("42".into()).to_string(),
163 "window '42' not found"
164 );
165 assert_eq!(
166 CaptureError::InvalidGeometry("bad".into()).to_string(),
167 "invalid geometry 'bad' (expected 'X,Y WxH')"
168 );
169 assert!(
170 CaptureError::WindowsUnsupported
171 .to_string()
172 .contains("wlroots >= 0.20")
173 );
174 }
175
176 #[test]
177 fn ambiguous_window_lists_every_candidate() {
178 let e = CaptureError::AmbiguousWindow {
179 selector: "app-id 'firefox'".into(),
180 candidates: vec![" 1 firefox Inbox".into(), " 2 firefox Docs".into()],
181 };
182 assert_eq!(
183 e.to_string(),
184 "app-id 'firefox' matches 2 windows:\n 1 firefox Inbox\n 2 firefox Docs"
185 );
186 }
187
188 #[cfg(feature = "video")]
189 #[test]
190 fn source_too_small_interpolates_dimensions() {
191 assert_eq!(
192 CaptureError::SourceTooSmall { w: 4, h: 2 }.to_string(),
193 "source too small to encode (4x2)"
194 );
195 }
196
197 #[test]
198 fn msg_is_a_backend_error_without_a_cause() {
199 let e = CaptureError::msg("boom");
200 assert_eq!(e.to_string(), "boom");
201 assert!(source(&e).is_none());
202 }
203
204 #[test]
205 fn context_on_result_wraps_and_preserves_the_cause() {
206 let e: CaptureError = "x".parse::<i32>().context("parsing width").unwrap_err();
208 assert_eq!(e.to_string(), "parsing width");
209 let cause = source(&e).expect("cause preserved");
210 assert!(cause.to_string().contains("invalid digit"));
211 }
212
213 #[test]
214 fn with_context_is_lazy_and_only_builds_on_error() {
215 let ok: Result<i32> = "1"
216 .parse::<i32>()
217 .with_context(|| -> String { unreachable!("not called on Ok") });
218 assert_eq!(ok.unwrap(), 1);
219
220 let e = "x"
221 .parse::<i32>()
222 .with_context(|| format!("ctx {}", 7))
223 .unwrap_err();
224 assert_eq!(e.to_string(), "ctx 7");
225 }
226
227 #[test]
228 fn context_on_option_maps_none_and_passes_some_through() {
229 let e = None::<i32>.context("missing thing").unwrap_err();
230 assert_eq!(e.to_string(), "missing thing");
231 assert!(source(&e).is_none());
232
233 assert_eq!(Some(5).context("unused").unwrap(), 5);
234 }
235}