1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, RustmotionError>;
5
6#[derive(Debug, Error)]
7pub enum RustmotionError {
8 #[error("{0}")]
9 Generic(String),
10
11 #[error("Failed to read '{path}': {source}")]
13 FileRead {
14 path: String,
15 source: std::io::Error,
16 },
17
18 #[error("Failed to parse JSON: {source}")]
20 JsonParse {
21 #[from]
22 source: serde_json::Error,
23 },
24
25 #[error("HTML transpile error: {0}")]
27 HtmlParse(String),
28
29 #[error("Failed to load image '{path}': {reason}")]
31 ImageLoad { path: String, reason: String },
32
33 #[error("Failed to decode image '{path}'")]
34 ImageDecode { path: String },
35
36 #[error("SVG component must have either 'src' or 'data'")]
37 SvgMissingSrc,
38
39 #[error("Failed to load SVG '{path}': {reason}")]
40 SvgLoad { path: String, reason: String },
41
42 #[error("Failed to parse SVG: {reason}")]
43 SvgParse { reason: String },
44
45 #[error("Failed to create pixmap for {target}")]
46 PixmapCreation { target: String },
47
48 #[error("Invalid icon format: '{icon}' (expected 'prefix:name')")]
49 InvalidIconFormat { icon: String },
50
51 #[error("Failed to fetch icon '{icon}': {reason}")]
52 IconFetch { icon: String, reason: String },
53
54 #[error("Failed to parse icon SVG '{icon}': {reason}")]
55 IconParse { icon: String, reason: String },
56
57 #[error("Failed to create Skia image from {target}")]
58 SkiaImageCreation { target: String },
59
60 #[error("Failed to open GIF '{path}': {reason}")]
61 GifOpen { path: String, reason: String },
62
63 #[error("Failed to decode GIF '{path}': {reason}")]
64 GifDecode { path: String, reason: String },
65
66 #[error("QR code generation failed: {reason}")]
67 QrCodeGeneration { reason: String },
68
69 #[error("No fonts available on this system")]
70 FontNotFound,
71
72 #[error(
74 "FontEntry for '{family}' has source=\"google\" but also sets 'path' — use one or the other"
75 )]
76 FontSourceAndPathConflict { family: String },
77
78 #[error(
79 "FontEntry for '{family}' requires either 'path' (local file) or 'source' (e.g. \"google\")"
80 )]
81 FontMissingPath { family: String },
82
83 #[error(
84 "Google Fonts: failed to fetch CSS for '{family}' (url: {url}): {reason}\n\
85 Tip: if you are offline, manually place a TTF for each weight at:\n {cache_hint}"
86 )]
87 GoogleFontsFetch {
88 family: String,
89 url: String,
90 reason: String,
91 cache_hint: String,
92 },
93
94 #[error(
95 "Google Fonts: no font URLs found in CSS response for '{family}' — \
96 the family name may be misspelled or unavailable"
97 )]
98 GoogleFontsNoUrls { family: String },
99
100 #[error("Google Fonts: failed to download TTF for '{family}' weight {weight}: {reason}")]
101 GoogleFontsTtfFetch {
102 family: String,
103 weight: u16,
104 reason: String,
105 },
106
107 #[error("Include depth limit ({limit}) exceeded while resolving '{path}'")]
109 IncludeDepthExceeded { limit: u8, path: String },
110
111 #[error("Include: scenes[{index}] is out of bounds in '{path}' (file has {total} scenes)")]
112 IncludeSceneOutOfBounds {
113 index: usize,
114 path: String,
115 total: usize,
116 },
117
118 #[error("Include: cannot resolve relative path '{path}' from inline JSON (use a file path or URL instead)")]
119 IncludeInlinePath { path: String },
120
121 #[error("Include: failed to fetch '{url}': {reason}")]
122 IncludeRemoteFetch { url: String, reason: String },
123
124 #[error("Include: file not found '{path}'")]
125 IncludeFileNotFound { path: String },
126
127 #[error(
128 "Scenario cannot have both top-level 'scenes' and 'composition' — use one or the other"
129 )]
130 CompositionAndScenesConflict,
131
132 #[error("Unknown background template '{name}' referenced via $ref")]
133 UnknownBackgroundTemplate { name: String },
134
135 #[error("Unknown heropattern '{name}' — see heropatterns.com for available patterns")]
136 UnknownHeropattern { name: String },
137
138 #[error("Variable '${name}' is not defined in '{path}'")]
140 UndefinedVariable { name: String, path: String },
141
142 #[error("Variable '{name}' in '{path}' is missing a default value")]
143 VariableMissingDefault { name: String, path: String },
144
145 #[error("Unresolved variable reference '${name}' after substitution in '{path}'")]
146 UnresolvedVariable { name: String, path: String },
147
148 #[error("Cannot interpolate non-string variable '${name}' into string in '{path}'")]
149 VariableInterpolationTypeError { name: String, path: String },
150
151 #[error("'components' at '{path}' must be an object mapping names to definitions")]
153 ComponentsBlockNotObject { path: String },
154
155 #[error("Component definition '{name}' at '{path}' is invalid: {reason}")]
156 ComponentDefinitionInvalid {
157 name: String,
158 path: String,
159 reason: String,
160 },
161
162 #[error("'use' directive at '{path}' is invalid: {reason}")]
163 UseDirectiveInvalid { path: String, reason: String },
164
165 #[error(
166 "Unknown component '{name}' referenced via 'use' at '{path}' — no such name in this \
167 file's 'components' block"
168 )]
169 UnknownComponent { name: String, path: String },
170
171 #[error(
172 "Missing required parameter '{param}' for component '{component}' at '{path}' — it has \
173 no default and was not supplied via 'props'"
174 )]
175 ComponentParamMissing {
176 component: String,
177 param: String,
178 path: String,
179 },
180
181 #[error(
182 "Unknown parameter '{param}' passed to component '{component}' at '{path}' — not \
183 declared in its 'params'"
184 )]
185 UnknownComponentParam {
186 component: String,
187 param: String,
188 path: String,
189 },
190
191 #[error("Component instantiation cycle at '{path}': {chain}")]
192 ComponentCycle { chain: String, path: String },
193
194 #[error("'for-each' directive at '{path}' is invalid: {reason}")]
195 ForEachDirectiveInvalid { path: String, reason: String },
196
197 #[error("'for-each' at '{path}' must resolve to an array; found {found}")]
198 ForEachNotArray { path: String, found: String },
199
200 #[error(
201 "Template/component expansion depth limit ({limit}) exceeded at '{path}' — likely a \
202 runaway nested 'use'/'for-each' template"
203 )]
204 ExpansionDepthExceeded { limit: u32, path: String },
205
206 #[error("No frames to render (total duration is 0)")]
208 NoFrames,
209
210 #[error("codec '{codec}' cannot be written into a .{container} file — {fix}")]
214 CodecContainerMismatch {
215 codec: String,
216 container: String,
217 fix: String,
218 },
219
220 #[error("Failed to run ffmpeg: {reason}. Is ffmpeg installed?")]
221 FfmpegSpawn { reason: String },
222
223 #[error("FFmpeg encoding failed{}", .stderr.as_ref().map(|s| format!(": {}", s)).unwrap_or_default())]
224 FfmpegFailed { stderr: Option<String> },
225
226 #[error("Failed to open FFmpeg stdin pipe")]
227 FfmpegPipe,
228
229 #[error("Failed to write to FFmpeg pipe: {reason}{}", .stderr.as_ref().map(|s| format!("\nffmpeg reported:\n{}", s)).unwrap_or_default())]
233 FfmpegWrite {
234 reason: String,
235 stderr: Option<String>,
236 },
237
238 #[error("Failed to wait for FFmpeg: {reason}")]
239 FfmpegWait { reason: String },
240
241 #[error("ffmpeg failed to extract frame from '{src}'")]
242 FfmpegFrameExtract { src: String },
243
244 #[error("Failed to create GIF encoder: {reason}")]
245 GifEncoder { reason: String },
246
247 #[error("Failed to set GIF repeat: {reason}")]
248 GifRepeat { reason: String },
249
250 #[error("Failed to write GIF frame: {reason}")]
251 GifFrame { reason: String },
252
253 #[error("Failed to open audio file '{path}': {reason}")]
255 AudioOpen { path: String, reason: String },
256
257 #[error("Failed to probe audio format for '{path}': {reason}")]
258 AudioProbe { path: String, reason: String },
259
260 #[error("No audio track found in '{path}'")]
261 AudioNoTrack { path: String },
262
263 #[error("Failed to create decoder for '{path}': {reason}")]
264 AudioDecoder { path: String, reason: String },
265
266 #[error("Failed to create Skia surface")]
268 SurfaceCreation,
269
270 #[error("Failed to create image from pixels")]
271 PixelImage,
272
273 #[error("Failed to read pixels from Skia surface")]
274 PixelRead,
275
276 #[error("Failed to create motion blur surface")]
277 MotionBlurSurface,
278
279 #[error("Cannot use both input file and --json")]
281 ConflictingInput,
282
283 #[error("Provide either an input file or --json")]
284 MissingInput,
285
286 #[error("--watch requires an input file path (cannot use --json or stdin)")]
287 WatchRequiresFile,
288
289 #[error("Frame {frame} is out of range (total frames: {total})")]
290 FrameOutOfRange { frame: u32, total: u32 },
291
292 #[error("Frame range {start}-{end} is out of range (total frames: {total})")]
293 FrameRangeOutOfRange { start: u32, end: u32, total: u32 },
294
295 #[error("Time {time:.2}s is beyond video duration")]
296 TimeOutOfRange { time: f64 },
297
298 #[error("File watcher channel closed")]
299 WatcherClosed,
300
301 #[error("Validation failed: {schema_errors} schema error(s), {geometry_violations} geometry violation(s), {unresolved_vars} unresolved variable(s). Run `rustmotion validate -f <file>` to see details.")]
302 ValidationFailed {
303 schema_errors: usize,
304 geometry_violations: usize,
305 unresolved_vars: usize,
306 },
307
308 #[error("Incremental encoding unsupported: {reason}")]
309 IncrementalUnsupported { reason: String },
310
311 #[error("Invalid CRF value {value}: must be between 0 and 51")]
312 InvalidCrf { value: u8 },
313
314 #[error("Unknown codec '{codec}'. Supported: h264, h265, vp9, prores")]
315 UnknownCodec { codec: String },
316
317 #[error("Path is not valid UTF-8: '{path}'")]
318 NonUtf8Path { path: String },
319
320 #[error("Failed to create preview window: {reason}")]
322 PreviewWindow { reason: String },
323
324 #[error("Failed to read Lottie file '{path}': {reason}")]
326 LottieRead { path: String, reason: String },
327
328 #[error("Lottie component requires either 'src' or 'data'")]
329 LottieMissingSrc,
330
331 #[error("Failed to read Lottie frame '{path}': {reason}")]
332 LottieFrameRead { path: String, reason: String },
333
334 #[error("Failed to decode Lottie frame '{path}': {reason}")]
335 LottieFrameDecode { path: String, reason: String },
336
337 #[error("Lottie render failed: {reason}")]
338 LottieRender { reason: String },
339
340 #[error(
342 "Unknown skill or rule: '{name}'. Run `rustmotion skills list` to see available rules."
343 )]
344 UnknownSkill { name: String },
345
346 #[error("{0}")]
348 Io(#[from] std::io::Error),
349
350 #[error("Image processing error: {0}")]
352 Image(#[from] image::ImageError),
353
354 #[error("File watcher error: {0}")]
355 Notify(#[from] notify::Error),
356
357 #[error("{0}")]
358 Other(String),
359}
360
361impl From<String> for RustmotionError {
362 fn from(s: String) -> Self {
363 RustmotionError::Other(s)
364 }
365}