1mod legacy;
37mod load;
38mod play;
39mod write;
40
41use std::borrow::Cow;
42use std::fmt;
43use std::time::Duration;
44
45use unicode_segmentation::UnicodeSegmentation;
46
47use crate::color::Rgb;
48use crate::icons::GlyphMode;
49use crate::theme::{Expr, MOTION_KEYS, Motion, Paint, Theme, parse_duration};
50
51pub(crate) use legacy::{LEGACY_ICONS, apply_legacy, check_legacy};
52pub use load::parse_animations;
53pub(crate) use load::read_animation_table;
54pub use play::CellFrame;
55
56pub const MAX_FRAMES: usize = 256;
58
59const BRACKETS: [char; 8] = ['[', ']', '(', ')', '{', '}', '<', '>'];
61
62pub fn check_glyph(glyph: &str, mode: GlyphMode) -> Result<(), String> {
69 if glyph.is_empty() {
70 return Err("the glyph is empty".to_owned());
71 }
72 if glyph.graphemes(true).count() != 1 {
73 return Err(format!("`{glyph}` is {} characters; a frame shows one", glyph.graphemes(true).count()));
74 }
75 let width = crate::text::width(glyph);
76 if width != 1 {
77 return Err(format!("`{glyph}` is {width} cells wide; a frame glyph must be exactly one cell"));
78 }
79 if mode == GlyphMode::Ascii && !glyph.chars().all(|c| c.is_ascii() && !c.is_ascii_control()) {
80 return Err(format!("`{glyph}` is not printable ASCII"));
81 }
82 if glyph.chars().any(|c| BRACKETS.contains(&c)) {
83 return Err(format!("`{glyph}` is a bracket; brackets are not allowed as glyphs"));
84 }
85 Ok(())
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct AnimatedCell {
92 pub glyph: String,
94 pub style: crate::style::CellStyle,
96 pub finished: bool,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash)]
103pub struct AnimationName(Cow<'static, str>);
104
105impl AnimationName {
106 #[must_use]
108 pub fn as_str(&self) -> &str {
109 &self.0
110 }
111}
112
113impl From<&'static str> for AnimationName {
114 fn from(name: &'static str) -> Self {
115 Self(Cow::Borrowed(name))
116 }
117}
118
119impl From<String> for AnimationName {
120 fn from(name: String) -> Self {
121 Self(Cow::Owned(name))
122 }
123}
124
125impl fmt::Display for AnimationName {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 f.write_str(&self.0)
128 }
129}
130
131#[must_use]
133pub fn is_valid_name(name: &str) -> bool {
134 !name.is_empty() && name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum FrameTime {
140 Motion(&'static str),
142 Fixed(Duration),
144}
145
146impl Default for FrameTime {
147 fn default() -> Self {
149 Self::Motion("spinner")
150 }
151}
152
153impl FrameTime {
154 pub fn parse(text: &str) -> Result<Self, String> {
160 let text = text.trim();
161 if let Some(key) = MOTION_KEYS.iter().find(|key| **key == text && **key != "slide") {
162 return Ok(Self::Motion(key));
163 }
164 if text.starts_with(|c: char| c.is_ascii_digit() || c == '.') {
165 let duration = parse_duration(text)?;
166 if duration.is_zero() {
167 return Err(format!("`{text}` is too short; a frame lasts longer than 0ms"));
168 }
169 return Ok(Self::Fixed(duration));
170 }
171 let keys: Vec<&str> = MOTION_KEYS.iter().copied().filter(|key| *key != "slide").collect();
172 Err(format!("`{text}` is not a frame time; use a motion key ({}) or a duration like \"80ms\"", keys.join(", ")))
173 }
174
175 #[must_use]
177 pub fn resolve(self, motion: &Motion) -> Duration {
178 let duration = match self {
179 Self::Motion(key) => motion.duration(key).unwrap_or(motion.spinner),
180 Self::Fixed(duration) => duration,
181 };
182 duration.max(Duration::from_millis(1))
183 }
184}
185
186impl fmt::Display for FrameTime {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 match self {
190 Self::Motion(key) => f.write_str(key),
191 Self::Fixed(duration) if duration.subsec_nanos() % 1_000_000 == 0 => {
192 write!(f, "{}ms", duration.as_millis())
193 }
194 Self::Fixed(duration) => write!(f, "{}s", duration.as_secs_f64()),
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
201pub enum Playback {
202 #[default]
204 Loop,
205 Once,
207 Bounce,
209}
210
211impl Playback {
212 pub const ALL: [Self; 3] = [Self::Loop, Self::Once, Self::Bounce];
214
215 #[must_use]
217 pub fn name(self) -> &'static str {
218 match self {
219 Self::Loop => "loop",
220 Self::Once => "once",
221 Self::Bounce => "bounce",
222 }
223 }
224
225 #[must_use]
227 pub fn from_name(name: &str) -> Option<Self> {
228 Self::ALL.into_iter().find(|playback| playback.name() == name)
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
234pub enum ColorMode {
235 #[default]
237 Step,
238 Blend,
240}
241
242impl ColorMode {
243 pub const ALL: [Self; 2] = [Self::Step, Self::Blend];
245
246 #[must_use]
248 pub fn name(self) -> &'static str {
249 match self {
250 Self::Step => "step",
251 Self::Blend => "blend",
252 }
253 }
254
255 #[must_use]
257 pub fn from_name(name: &str) -> Option<Self> {
258 Self::ALL.into_iter().find(|mode| mode.name() == name)
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct CellColor {
265 text: String,
266 expr: Expr,
267}
268
269impl CellColor {
270 pub fn parse(text: &str) -> Result<Self, String> {
276 let expr = Expr::parse(text)?;
277 if let Some(message) = expr.nested_pulse() {
278 return Err(message);
279 }
280 Ok(Self { text: text.trim().to_owned(), expr })
281 }
282
283 #[must_use]
285 pub fn as_str(&self) -> &str {
286 &self.text
287 }
288
289 pub fn resolve(&self, theme: &Theme, fg: Rgb) -> Result<Paint, String> {
295 self.expr.resolve_by(&|name| if name == "fg" { Some(fg) } else { theme.color(name) })
296 }
297}
298
299impl From<Rgb> for CellColor {
300 fn from(color: Rgb) -> Self {
302 Self { text: format!("#{:02X}{:02X}{:02X}", color.r, color.g, color.b), expr: Expr::Hex(color) }
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct AnimationFrame {
309 ascii: String,
310 unicode: Option<String>,
311 nerd: Option<String>,
312 color: Option<CellColor>,
313 duration: Option<FrameTime>,
314}
315
316impl AnimationFrame {
317 #[must_use]
321 pub fn new(ascii: impl Into<String>) -> Self {
322 Self { ascii: ascii.into(), unicode: None, nerd: None, color: None, duration: None }
323 }
324
325 #[must_use]
327 pub fn unicode(mut self, glyph: impl Into<String>) -> Self {
328 self.unicode = Some(glyph.into());
329 self
330 }
331
332 #[must_use]
334 pub fn nerd(mut self, glyph: impl Into<String>) -> Self {
335 self.nerd = Some(glyph.into());
336 self
337 }
338
339 #[must_use]
341 pub fn color(mut self, color: CellColor) -> Self {
342 self.color = Some(color);
343 self
344 }
345
346 #[must_use]
348 pub fn duration(mut self, duration: FrameTime) -> Self {
349 self.duration = Some(duration);
350 self
351 }
352
353 #[must_use]
355 pub fn glyph(&self, mode: GlyphMode) -> &str {
356 let unicode = || self.unicode.as_deref().unwrap_or(&self.ascii);
357 match mode {
358 GlyphMode::Nerd => self.nerd.as_deref().unwrap_or_else(unicode),
359 GlyphMode::Unicode => unicode(),
360 GlyphMode::Ascii => &self.ascii,
361 }
362 }
363
364 #[must_use]
366 pub fn own_glyph(&self, mode: GlyphMode) -> Option<&str> {
367 match mode {
368 GlyphMode::Nerd => self.nerd.as_deref(),
369 GlyphMode::Unicode => self.unicode.as_deref(),
370 GlyphMode::Ascii => Some(&self.ascii),
371 }
372 }
373
374 #[must_use]
376 pub fn frame_color(&self) -> Option<&CellColor> {
377 self.color.as_ref()
378 }
379
380 #[must_use]
382 pub fn frame_duration(&self) -> Option<FrameTime> {
383 self.duration
384 }
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Default)]
399pub struct CellAnimation {
400 frames: Vec<AnimationFrame>,
401 frame_time: FrameTime,
402 playback: Playback,
403 colors: ColorMode,
404 rest: Option<usize>,
405}
406
407impl CellAnimation {
408 #[must_use]
410 pub fn new() -> Self {
411 Self::default()
412 }
413
414 #[must_use]
416 pub fn frame(mut self, frame: AnimationFrame) -> Self {
417 self.frames.push(frame);
418 self
419 }
420
421 #[must_use]
423 pub fn frame_time(mut self, time: FrameTime) -> Self {
424 self.frame_time = time;
425 self
426 }
427
428 #[must_use]
430 pub fn playback(mut self, playback: Playback) -> Self {
431 self.playback = playback;
432 self
433 }
434
435 #[must_use]
437 pub fn colors(mut self, colors: ColorMode) -> Self {
438 self.colors = colors;
439 self
440 }
441
442 #[must_use]
445 pub fn rest(mut self, index: usize) -> Self {
446 self.rest = Some(index);
447 self
448 }
449
450 #[must_use]
452 pub fn frames(&self) -> &[AnimationFrame] {
453 &self.frames
454 }
455
456 #[must_use]
458 pub fn time(&self) -> FrameTime {
459 self.frame_time
460 }
461
462 #[must_use]
464 pub fn play_mode(&self) -> Playback {
465 self.playback
466 }
467
468 #[must_use]
470 pub fn color_mode(&self) -> ColorMode {
471 self.colors
472 }
473
474 #[must_use]
476 pub fn rest_frame(&self) -> Option<usize> {
477 self.rest
478 }
479
480 #[must_use]
483 pub fn rest_index(&self) -> usize {
484 let last = self.frames.len().saturating_sub(1);
485 self.rest.unwrap_or(if self.playback == Playback::Once { last } else { 0 }).min(last)
486 }
487
488 #[must_use]
490 pub fn glyph(&self, index: usize, mode: GlyphMode) -> &str {
491 self.frames.get(index).map_or("", |frame| frame.glyph(mode))
492 }
493}
494
495#[cfg(test)]
496mod tests;