Skip to main content

qframe/icons/
mod.rs

1//! Icon sets: every icon has a Nerd Font, a Unicode and an ASCII glyph, and the terminal's
2//! capabilities decide which one is drawn.
3//!
4//! ```toml
5//! [meta]
6//! name = "Default"
7//!
8//! [icons]
9//! check = { nerd = "", unicode = "✓", ascii = "v" }
10//!
11//! [animations.blink]
12//! frames = [{ unicode = "●", ascii = "*" }, { unicode = "·", ascii = "." }]
13//! ```
14//!
15//! An application's own icon sets add their new keys to every set: `category.internet` from an
16//! application's file is drawn whatever set the theme chooses, see [`IconSetRegistry`]. A missing
17//! Nerd Font or Unicode glyph is reported with its file, line and column, and a plainer glyph of
18//! the same icon stands in for it.
19//!
20//! An icon set also holds one-cell animations (see [`crate::animation`]); themes
21//! replace single animations the way they replace single icons.
22
23mod detect;
24pub mod nerd_font;
25mod sample;
26
27use std::borrow::Cow;
28use std::collections::BTreeMap;
29use std::io;
30use std::path::Path;
31use std::sync::Arc;
32
33use toml::de::DeTable;
34use unicode_segmentation::UnicodeSegmentation;
35
36pub use detect::{default_font_dirs, detect_glyph_mode};
37pub use sample::GlyphSample;
38
39use crate::animation::{self, CellAnimation};
40use crate::assets;
41use crate::diagnostics::Diagnostic;
42use crate::doc::{self, Doc, Value};
43
44/// The glyphs of one icon.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct IconGlyphs {
47    /// Glyph for terminals with a Nerd Font.
48    pub nerd: String,
49    /// Glyph for UTF-8 terminals without a Nerd Font.
50    pub unicode: String,
51    /// Glyph for terminals that can only show ASCII.
52    pub ascii: String,
53}
54
55impl IconGlyphs {
56    /// The glyph for `mode`.
57    #[must_use]
58    pub fn for_mode(&self, mode: GlyphMode) -> &str {
59        match mode {
60            GlyphMode::Nerd => &self.nerd,
61            GlyphMode::Unicode => &self.unicode,
62            GlyphMode::Ascii => &self.ascii,
63        }
64    }
65}
66
67/// The icon preference a user or application chooses.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub enum IconMode {
70    /// Detect from the terminal and installed fonts.
71    #[default]
72    Auto,
73    /// Always use Nerd Font glyphs.
74    Nerd,
75    /// Always use Unicode glyphs.
76    Unicode,
77    /// Always use ASCII glyphs.
78    Ascii,
79}
80
81impl IconMode {
82    /// Every mode, in the order a settings screen lists them.
83    pub const ALL: [Self; 4] = [Self::Auto, Self::Nerd, Self::Unicode, Self::Ascii];
84
85    /// The name used in settings and the `QUVYTA_ICONS` environment variable.
86    #[must_use]
87    pub fn name(self) -> &'static str {
88        match self {
89            Self::Auto => "auto",
90            Self::Nerd => "nerd",
91            Self::Unicode => "unicode",
92            Self::Ascii => "ascii",
93        }
94    }
95
96    /// Looks a mode up by name, ignoring letter case.
97    #[must_use]
98    pub fn from_name(name: &str) -> Option<Self> {
99        let name = name.trim().to_ascii_lowercase();
100        Self::ALL.into_iter().find(|mode| mode.name() == name)
101    }
102}
103
104/// The glyph column actually drawn.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum GlyphMode {
107    /// Nerd Font glyphs.
108    Nerd,
109    /// Unicode glyphs.
110    Unicode,
111    /// ASCII glyphs.
112    Ascii,
113}
114
115/// Characters an ASCII glyph may not contain: glyphs must not fake shapes with brackets.
116const BANNED_ASCII: [char; 6] = ['[', ']', '(', ')', '{', '}'];
117
118/// Reads `{ nerd = "…", unicode = "…", ascii = "…" }`.
119///
120/// A missing column is reported where the icon is, and a plainer glyph stands in for it: the
121/// Unicode glyph for a missing Nerd Font glyph, since a Nerd Font draws Unicode too, and the ASCII
122/// glyph for a missing Unicode one. Nothing plainer can stand in for ASCII, so an icon without its
123/// ASCII glyph is an error and is skipped; the warnings for stand-ins go to `report`.
124pub(crate) fn parse_glyphs(
125    doc: &Doc<'_>,
126    key: &str,
127    value: &Value<'_>,
128    report: &mut Vec<Diagnostic>,
129) -> Result<IconGlyphs, Diagnostic> {
130    let table = doc.table(value, &format!("icon `{key}`"))?;
131    let field = |name: &str| -> Result<Option<String>, Diagnostic> {
132        let Some(entry) = doc::get(table, name) else {
133            return Ok(None);
134        };
135        let text = doc.string(entry, &format!("icon `{key}`.{name}"))?;
136        if text.is_empty() {
137            return Err(doc.error(&entry.span(), format!("icon `{key}`.{name} must not be empty")));
138        }
139        Ok(Some(text.to_owned()))
140    };
141    let (nerd, unicode, ascii) = (field("nerd")?, field("unicode")?, field("ascii")?);
142    if let Some((unknown, entry)) =
143        table.iter().find(|(name, _)| !["nerd", "unicode", "ascii"].contains(&name.get_ref().as_ref()))
144    {
145        return Err(doc.error(
146            &entry.span(),
147            format!("icon `{key}` has unknown field `{}`; use nerd, unicode and ascii", unknown.get_ref()),
148        ));
149    }
150    let Some(ascii) = ascii else {
151        return Err(doc.error(
152            &value.span(),
153            format!("icon `{key}` is missing its `ascii` glyph, which every terminal can draw; the icon is skipped"),
154        ));
155    };
156    let ascii_ok = ascii.chars().all(|c| c.is_ascii() && !c.is_ascii_control());
157    if !ascii_ok {
158        return Err(doc.error(&value.span(), format!("icon `{key}`.ascii must contain only printable ASCII")));
159    }
160    if let Some(bad) = ascii.chars().find(|c| BANNED_ASCII.contains(c)) {
161        return Err(
162            doc.error(&value.span(), format!("icon `{key}`.ascii uses `{bad}`; brackets are not allowed as glyphs"))
163        );
164    }
165    let mut stand_in = |missing: &str, used: &str| {
166        report.push(doc.warning(
167            &value.span(),
168            format!("icon `{key}` is missing its `{missing}` glyph; its `{used}` glyph stands in"),
169        ));
170    };
171    let (unicode, plainer) = match unicode {
172        Some(unicode) => (unicode, "unicode"),
173        None => {
174            stand_in("unicode", "ascii");
175            (ascii.clone(), "ascii")
176        }
177    };
178    let nerd = nerd.unwrap_or_else(|| {
179        stand_in("nerd", plainer);
180        unicode.clone()
181    });
182    Ok(IconGlyphs { nerd, unicode, ascii })
183}
184
185/// The icon drawn at the left of hovered, focused and selected rows, tabs, buttons and cards.
186pub const PILLAR: &str = "pillar";
187
188/// A pillar a user can choose at runtime, over whatever the theme draws.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum PillarStyle {
191    /// A half block, `▌`: the default.
192    Thick,
193    /// A quarter block, `▎`.
194    Thin,
195}
196
197impl PillarStyle {
198    /// Every style, in the order a settings screen lists them.
199    pub const ALL: [Self; 2] = [Self::Thick, Self::Thin];
200
201    /// The name used in settings and theme files.
202    #[must_use]
203    pub fn name(self) -> &'static str {
204        match self {
205            Self::Thick => "thick",
206            Self::Thin => "thin",
207        }
208    }
209
210    /// Looks a style up by name, ignoring letter case.
211    #[must_use]
212    pub fn from_name(name: &str) -> Option<Self> {
213        let name = name.trim().to_ascii_lowercase();
214        Self::ALL.into_iter().find(|style| style.name() == name)
215    }
216
217    /// The glyph drawn for this style; ASCII terminals always draw a coloured cell.
218    #[must_use]
219    pub fn glyphs(self) -> IconGlyphs {
220        pillar_glyphs(match self {
221            Self::Thick => "▌",
222            Self::Thin => "▎",
223        })
224    }
225}
226
227/// A pillar drawn with `glyph` wherever Unicode is available and as a coloured cell in ASCII.
228fn pillar_glyphs(glyph: &str) -> IconGlyphs {
229    IconGlyphs { nerd: glyph.to_owned(), unicode: glyph.to_owned(), ascii: " ".to_owned() }
230}
231
232/// Reads the shorthand of the pillar: `"thick"` (`▌`), `"thin"` (`▎`) or any single-cell
233/// character. ASCII terminals always show the pillar as a coloured cell.
234fn parse_pillar(doc: &Doc<'_>, value: &Value<'_>) -> Result<IconGlyphs, Diagnostic> {
235    let text = doc.string(value, "icon `pillar`")?;
236    if let Some(style) = PillarStyle::from_name(text) {
237        return Ok(style.glyphs());
238    }
239    if crate::text::width(text) == 1 && text.chars().count() == 1 {
240        Ok(pillar_glyphs(text))
241    } else {
242        Err(doc.error(
243            &value.span(),
244            format!("icon `pillar` is `{text}`; use \"thick\", \"thin\" or a single one-cell character"),
245        ))
246    }
247}
248
249/// Reads an `[icons]` table into `glyphs`, reporting and skipping broken entries. The pillar may
250/// also be given in its short form, see [`PILLAR`].
251pub(crate) fn read_icon_table(
252    doc: &Doc<'_>,
253    table: &DeTable<'_>,
254    glyphs: &mut BTreeMap<String, IconGlyphs>,
255    report: &mut Vec<Diagnostic>,
256) {
257    for (key, value) in table {
258        let parsed = if key.get_ref() == PILLAR && value.get_ref().as_str().is_some() {
259            parse_pillar(doc, value)
260        } else {
261            parse_glyphs(doc, key.get_ref(), value, report)
262                .and_then(|glyphs| legacy_glyphs(doc, key.get_ref(), value, glyphs))
263        };
264        match parsed {
265            Ok(parsed) => {
266                glyphs.insert(key.get_ref().to_string(), parsed);
267            }
268            Err(diagnostic) => report.push(diagnostic),
269        }
270    }
271}
272
273/// Icon keys that were renamed, with the key each is now. A former name still gives the same
274/// glyph, so an application that asks for it keeps its icon until it moves to the new name; the
275/// list is not shown among the set's [`keys`](Icons::keys).
276const FORMER_KEYS: &[(&str, &str)] = &[("family", "ecosystem")];
277
278/// Checks the frames of a former spinner icon, which now replaces an animation's glyphs.
279fn legacy_glyphs(doc: &Doc<'_>, key: &str, value: &Value<'_>, glyphs: IconGlyphs) -> Result<IconGlyphs, Diagnostic> {
280    if !animation::LEGACY_ICONS.iter().any(|(icon, _)| *icon == key) {
281        return Ok(glyphs);
282    }
283    animation::check_legacy(&glyphs).map_err(|message| doc.error(&value.span(), format!("icon `{key}`: {message}")))?;
284    Ok(glyphs)
285}
286
287/// Adds one layer of animations over `animations`: first the former spinner icons among `glyphs`,
288/// then the layer's own animations, which win over icons of the same layer.
289fn layer_animations(
290    animations: &mut BTreeMap<String, Arc<CellAnimation>>,
291    glyphs: &BTreeMap<String, IconGlyphs>,
292    own: impl IntoIterator<Item = (String, Arc<CellAnimation>)>,
293) {
294    for (icon, name) in animation::LEGACY_ICONS {
295        if let Some(glyphs) = glyphs.get(icon) {
296            animation::apply_legacy(animations, name, glyphs);
297        }
298    }
299    animations.extend(own);
300}
301
302/// A loaded icon set file.
303#[derive(Debug, Clone)]
304struct IconSetSource {
305    name: String,
306    glyphs: BTreeMap<String, IconGlyphs>,
307    animations: BTreeMap<String, Arc<CellAnimation>>,
308}
309
310/// All icon sets known to an application: the built-in ones plus the application's own.
311///
312/// A set is drawn when a theme names it (`[meta] icon-set`). Keys of the application's sets that
313/// the built-in set does not have, such as `category.internet`, are drawn whatever set is chosen:
314/// they sit under the chosen set, so a theme's set or single icon can still restyle them, and a
315/// set added later wins a key two application sets give. A key the built-in set already has, such
316/// as `check`, is a restyling of the framework's own icon, which every widget draws; it applies
317/// only while its set is the chosen one, so an application set never changes the icons of a set
318/// the user picked.
319#[derive(Debug, Clone)]
320pub struct IconSetRegistry {
321    sets: BTreeMap<String, IconSetSource>,
322    /// Ids of the sets added after the built-in ones, oldest first.
323    added: Vec<String>,
324    diagnostics: Vec<Diagnostic>,
325}
326
327impl IconSetRegistry {
328    /// A registry holding the built-in icon sets.
329    #[must_use]
330    pub fn builtin() -> Self {
331        let mut registry = Self { sets: BTreeMap::new(), added: Vec::new(), diagnostics: Vec::new() };
332        for (id, text) in assets::ICON_SETS {
333            registry.add_source(id, &format!("{id}.toml"), text);
334        }
335        registry.added.clear();
336        registry
337    }
338
339    /// Adds or replaces the icon set `id` from TOML text. Returns whether it was usable.
340    ///
341    /// Its keys the built-in set lacks are drawn in every set from then on; see
342    /// [`IconSetRegistry`] for how it layers with the chosen set.
343    pub fn add_source(&mut self, id: &str, file: &str, text: &str) -> bool {
344        let doc = Doc::new(file, text);
345        let root = match doc.parse() {
346            Ok(root) => root,
347            Err(diagnostic) => {
348                self.diagnostics.push(diagnostic);
349                return false;
350            }
351        };
352        for (key, value) in &root {
353            if !["meta", "icons", "animations"].contains(&key.get_ref().as_ref()) {
354                self.diagnostics.push(doc.error(
355                    &value.span(),
356                    format!("unknown section `{}`; expected meta, icons and animations", key.get_ref()),
357                ));
358            }
359        }
360        let name = self.read_name(&doc, &root).unwrap_or_else(|| id.to_owned());
361        let mut glyphs = BTreeMap::new();
362        match doc::get(&root, "icons") {
363            Some(icons) => match doc.table(icons, "icons") {
364                Ok(table) => read_icon_table(&doc, table, &mut glyphs, &mut self.diagnostics),
365                Err(diagnostic) => self.diagnostics.push(diagnostic),
366            },
367            None => self.diagnostics.push(Diagnostic::error(None, format!("{file}: missing [icons] table"))),
368        }
369        let mut animations = BTreeMap::new();
370        if let Some(table) = doc::get(&root, "animations") {
371            match doc.table(table, "animations") {
372                Ok(table) => animation::read_animation_table(&doc, table, &mut animations, &mut self.diagnostics),
373                Err(diagnostic) => self.diagnostics.push(diagnostic),
374            }
375        }
376        let animations = animations.into_iter().map(|(name, animation)| (name, Arc::new(animation))).collect();
377        self.sets.insert(id.to_owned(), IconSetSource { name, glyphs, animations });
378        self.added.retain(|added| added != id);
379        self.added.push(id.to_owned());
380        true
381    }
382
383    /// The display name from `[meta] name`, reporting a malformed `[meta]` and unknown keys in it.
384    fn read_name(&mut self, doc: &Doc<'_>, root: &DeTable<'_>) -> Option<String> {
385        let meta = match doc.table(doc::get(root, "meta")?, "meta") {
386            Ok(meta) => meta,
387            Err(diagnostic) => {
388                self.diagnostics.push(diagnostic);
389                return None;
390            }
391        };
392        let mut name = None;
393        for (key, value) in meta {
394            if key.get_ref() != "name" {
395                self.diagnostics.push(doc.error(&value.span(), format!("unknown key `meta.{}`", key.get_ref())));
396                continue;
397            }
398            match doc.string(value, "meta.name") {
399                Ok(text) => name = Some(text.to_owned()),
400                Err(diagnostic) => self.diagnostics.push(diagnostic),
401            }
402        }
403        name
404    }
405
406    /// Loads every `*.toml` file in `dir`; the file stem is the set id.
407    ///
408    /// # Errors
409    ///
410    /// Returns the I/O error when the directory cannot be read. A file that cannot be read is
411    /// skipped and reported in the diagnostics.
412    pub fn load_dir(&mut self, dir: &Path) -> io::Result<()> {
413        let found = assets::read_toml_dir(dir)?;
414        self.diagnostics.extend(found.skipped);
415        for (id, file, text) in found.files {
416            self.add_source(&id, &file, &text);
417        }
418        Ok(())
419    }
420
421    /// `(id, display name)` of every set, sorted by id.
422    #[must_use]
423    pub fn list(&self) -> Vec<(String, String)> {
424        self.sets.iter().map(|(id, set)| (id.clone(), set.name.clone())).collect()
425    }
426
427    /// Problems found while loading.
428    #[must_use]
429    pub fn diagnostics(&self) -> &[Diagnostic] {
430        &self.diagnostics
431    }
432
433    /// Builds the icons for set `id` with `overrides` applied on top.
434    ///
435    /// An unknown id falls back to the built-in `default` set.
436    #[must_use]
437    pub fn icons(&self, id: &str, overrides: &BTreeMap<String, IconGlyphs>, mode: GlyphMode) -> Icons {
438        self.icons_with_animations(id, overrides, &BTreeMap::new(), mode)
439    }
440
441    /// Builds the icons and animations for set `id`, with icon `overrides` and `animations` (such
442    /// as a theme's) applied on top.
443    ///
444    /// Animations layer from the built-in `default` set, through set `id`, to the overrides, so a
445    /// set or theme without animations still has every built-in one. Within a layer, a former
446    /// spinner icon key such as `spinner-arc` first replaces the glyphs of its animation, then the
447    /// layer's own animations apply. An unknown id falls back to the built-in `default` set.
448    #[must_use]
449    pub fn icons_with_animations(
450        &self,
451        id: &str,
452        overrides: &BTreeMap<String, IconGlyphs>,
453        animations: &BTreeMap<String, Arc<CellAnimation>>,
454        mode: GlyphMode,
455    ) -> Icons {
456        let fallback = self.sets.get("default");
457        let chosen = self.sets.get(id);
458        let owned = |source: &IconSetSource| source.animations.clone();
459        let mut layered = BTreeMap::new();
460        if let Some(default) = fallback {
461            layer_animations(&mut layered, &default.glyphs, owned(default));
462        }
463        if let Some(set) = chosen.filter(|_| id != "default") {
464            layer_animations(&mut layered, &set.glyphs, owned(set));
465        }
466        layer_animations(&mut layered, overrides, animations.clone());
467        let mut glyphs = self.application_keys();
468        glyphs.extend(chosen.or(fallback).map(|set| set.glyphs.clone()).unwrap_or_default());
469        glyphs.extend(overrides.iter().map(|(k, v)| (k.clone(), v.clone())));
470        glyphs.retain(|key, _| !animation::LEGACY_ICONS.iter().any(|(icon, _)| icon == key));
471        Icons { glyphs, animations: layered, mode }
472    }
473
474    /// The keys the application's sets add to the built-in set, a later set winning a key two of
475    /// them give.
476    fn application_keys(&self) -> BTreeMap<String, IconGlyphs> {
477        let builtin = self.sets.get("default").map(|set| &set.glyphs);
478        let mut keys = BTreeMap::new();
479        for set in self.added.iter().filter_map(|id| self.sets.get(id)) {
480            let new = set.glyphs.iter().filter(|(key, _)| builtin.is_none_or(|builtin| !builtin.contains_key(*key)));
481            keys.extend(new.map(|(key, glyphs)| (key.clone(), glyphs.clone())));
482        }
483        keys
484    }
485}
486
487/// A glyph drawn before a label: an icon of the icon set, which follows the theme and the glyph
488/// mode, or a glyph the application gives as it is, such as a Nerd Font code point it looked up in
489/// its own table.
490///
491/// Text converts into a key, so `cell.icon("folder", None)` reads as before.
492///
493/// ```
494/// use qframe::env::Env;
495/// use qframe::icons::Glyph;
496///
497/// let icons = Env::builtin().icons().clone();
498/// assert_eq!(Glyph::key("check").resolve(&icons), "✓");
499/// assert_eq!(Glyph::literal('\u{e745}').resolve(&icons), "\u{e745}");
500/// ```
501#[derive(Debug, Clone, PartialEq, Eq)]
502pub enum Glyph {
503    /// The icon `key` of the icon set, drawn in the glyph mode in use.
504    Key(String),
505    /// This text, drawn as it is in every glyph mode.
506    Literal(String),
507}
508
509impl Glyph {
510    /// The icon `key` of the icon set, such as `"folder"` or an application's `"category.internet"`.
511    #[must_use]
512    pub fn key(key: impl Into<String>) -> Self {
513        Self::Key(key.into())
514    }
515
516    /// A glyph drawn as it is, such as `'\u{e745}'`. The application answers for the glyph mode: a
517    /// Nerd Font code point only belongs on screen when [`Env::glyph_mode`](crate::env::Env::glyph_mode)
518    /// is [`GlyphMode::Nerd`].
519    #[must_use]
520    pub fn literal(glyph: impl Into<String>) -> Self {
521        Self::Literal(glyph.into())
522    }
523
524    /// The text drawn for this glyph with `icons`.
525    #[must_use]
526    pub fn resolve<'a>(&'a self, icons: &'a Icons) -> Cow<'a, str> {
527        match self {
528            Self::Key(key) => icons.glyph(key),
529            Self::Literal(glyph) => Cow::Borrowed(glyph),
530        }
531    }
532}
533
534impl From<&str> for Glyph {
535    fn from(key: &str) -> Self {
536        Self::key(key)
537    }
538}
539
540impl From<String> for Glyph {
541    fn from(key: String) -> Self {
542        Self::Key(key)
543    }
544}
545
546/// Icons ready to draw in one glyph mode.
547#[derive(Debug, Clone, PartialEq, Eq)]
548pub struct Icons {
549    glyphs: BTreeMap<String, IconGlyphs>,
550    animations: BTreeMap<String, Arc<CellAnimation>>,
551    mode: GlyphMode,
552}
553
554impl Icons {
555    /// The glyph mode in use.
556    #[must_use]
557    pub fn mode(&self) -> GlyphMode {
558        self.mode
559    }
560
561    /// Switches glyph mode.
562    pub fn set_mode(&mut self, mode: GlyphMode) {
563        self.mode = mode;
564    }
565
566    /// The glyphs of `key`, or of the key it was renamed to when `key` is a former name.
567    fn lookup(&self, key: &str) -> Option<&IconGlyphs> {
568        self.glyphs.get(key).or_else(|| {
569            let (_, now) = FORMER_KEYS.iter().find(|(former, _)| *former == key)?;
570            self.glyphs.get(*now)
571        })
572    }
573
574    /// The glyph for `key`. A missing icon is drawn as `⟦key⟧` so it is noticed.
575    #[must_use]
576    pub fn glyph(&self, key: &str) -> Cow<'_, str> {
577        match self.lookup(key) {
578            Some(glyphs) => Cow::Borrowed(glyphs.for_mode(self.mode)),
579            None => Cow::Owned(format!("⟦{key}⟧")),
580        }
581    }
582
583    /// The glyph for `key` split into animation frames, one grapheme each.
584    #[must_use]
585    pub fn frames(&self, key: &str) -> Vec<String> {
586        self.glyph(key).graphemes(true).map(str::to_owned).collect()
587    }
588
589    /// Every glyph of `key`, whatever the mode, or `None` when it is not defined.
590    #[must_use]
591    pub fn glyphs(&self, key: &str) -> Option<&IconGlyphs> {
592        self.lookup(key)
593    }
594
595    /// Whether `key` is defined.
596    #[must_use]
597    pub fn contains(&self, key: &str) -> bool {
598        self.lookup(key).is_some()
599    }
600
601    /// Every icon key, sorted.
602    pub fn keys(&self) -> impl Iterator<Item = &str> {
603        self.glyphs.keys().map(String::as_str)
604    }
605
606    /// The animation named `name`, such as `"spinner-arc"`.
607    #[must_use]
608    pub fn animation(&self, name: &str) -> Option<&Arc<CellAnimation>> {
609        self.animations.get(name)
610    }
611
612    /// Every animation name, sorted.
613    pub fn animation_names(&self) -> impl Iterator<Item = &str> {
614        self.animations.keys().map(String::as_str)
615    }
616}
617
618#[cfg(test)]
619mod tests;