1mod detect;
24mod kinds;
25pub mod nerd_font;
26mod sample;
27
28use std::borrow::Cow;
29use std::collections::BTreeMap;
30use std::io;
31use std::path::Path;
32use std::sync::Arc;
33
34use toml::de::DeTable;
35use unicode_segmentation::UnicodeSegmentation;
36
37pub use detect::{default_font_dirs, detect_glyph_mode};
38pub use kinds::{FileKind, KindFamily, UserFolders, file_kind};
39pub use sample::GlyphSample;
40
41use crate::animation::{self, CellAnimation};
42use crate::assets;
43use crate::diagnostics::Diagnostic;
44use crate::doc::{self, Doc, Value};
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct IconGlyphs {
49 pub nerd: String,
51 pub unicode: String,
53 pub ascii: String,
55}
56
57impl IconGlyphs {
58 #[must_use]
60 pub fn for_mode(&self, mode: GlyphMode) -> &str {
61 match mode {
62 GlyphMode::Nerd => &self.nerd,
63 GlyphMode::Unicode => &self.unicode,
64 GlyphMode::Ascii => &self.ascii,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum IconMode {
72 #[default]
74 Auto,
75 Nerd,
77 Unicode,
79 Ascii,
81}
82
83impl IconMode {
84 pub const ALL: [Self; 4] = [Self::Auto, Self::Nerd, Self::Unicode, Self::Ascii];
86
87 #[must_use]
89 pub fn name(self) -> &'static str {
90 match self {
91 Self::Auto => "auto",
92 Self::Nerd => "nerd",
93 Self::Unicode => "unicode",
94 Self::Ascii => "ascii",
95 }
96 }
97
98 #[must_use]
100 pub fn from_name(name: &str) -> Option<Self> {
101 let name = name.trim().to_ascii_lowercase();
102 Self::ALL.into_iter().find(|mode| mode.name() == name)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum GlyphMode {
109 Nerd,
111 Unicode,
113 Ascii,
115}
116
117const BANNED_ASCII: [char; 6] = ['[', ']', '(', ')', '{', '}'];
119
120pub(crate) fn parse_glyphs(
127 doc: &Doc<'_>,
128 key: &str,
129 value: &Value<'_>,
130 report: &mut Vec<Diagnostic>,
131) -> Result<IconGlyphs, Diagnostic> {
132 let table = doc.table(value, &format!("icon `{key}`"))?;
133 let field = |name: &str| -> Result<Option<String>, Diagnostic> {
134 let Some(entry) = doc::get(table, name) else {
135 return Ok(None);
136 };
137 let text = doc.string(entry, &format!("icon `{key}`.{name}"))?;
138 if text.is_empty() {
139 return Err(doc.error(&entry.span(), format!("icon `{key}`.{name} must not be empty")));
140 }
141 Ok(Some(text.to_owned()))
142 };
143 let (nerd, unicode, ascii) = (field("nerd")?, field("unicode")?, field("ascii")?);
144 if let Some((unknown, entry)) =
145 table.iter().find(|(name, _)| !["nerd", "unicode", "ascii"].contains(&name.get_ref().as_ref()))
146 {
147 return Err(doc.error(
148 &entry.span(),
149 format!("icon `{key}` has unknown field `{}`; use nerd, unicode and ascii", unknown.get_ref()),
150 ));
151 }
152 let Some(ascii) = ascii else {
153 return Err(doc.error(
154 &value.span(),
155 format!("icon `{key}` is missing its `ascii` glyph, which every terminal can draw; the icon is skipped"),
156 ));
157 };
158 let ascii_ok = ascii.chars().all(|c| c.is_ascii() && !c.is_ascii_control());
159 if !ascii_ok {
160 return Err(doc.error(&value.span(), format!("icon `{key}`.ascii must contain only printable ASCII")));
161 }
162 if let Some(bad) = ascii.chars().find(|c| BANNED_ASCII.contains(c)) {
163 return Err(
164 doc.error(&value.span(), format!("icon `{key}`.ascii uses `{bad}`; brackets are not allowed as glyphs"))
165 );
166 }
167 let mut stand_in = |missing: &str, used: &str| {
168 report.push(doc.warning(
169 &value.span(),
170 format!("icon `{key}` is missing its `{missing}` glyph; its `{used}` glyph stands in"),
171 ));
172 };
173 let (unicode, plainer) = match unicode {
174 Some(unicode) => (unicode, "unicode"),
175 None => {
176 stand_in("unicode", "ascii");
177 (ascii.clone(), "ascii")
178 }
179 };
180 let nerd = nerd.unwrap_or_else(|| {
181 stand_in("nerd", plainer);
182 unicode.clone()
183 });
184 Ok(IconGlyphs { nerd, unicode, ascii })
185}
186
187pub const PILLAR: &str = "pillar";
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum PillarStyle {
193 Thick,
195 Thin,
197}
198
199impl PillarStyle {
200 pub const ALL: [Self; 2] = [Self::Thick, Self::Thin];
202
203 #[must_use]
205 pub fn name(self) -> &'static str {
206 match self {
207 Self::Thick => "thick",
208 Self::Thin => "thin",
209 }
210 }
211
212 #[must_use]
214 pub fn from_name(name: &str) -> Option<Self> {
215 let name = name.trim().to_ascii_lowercase();
216 Self::ALL.into_iter().find(|style| style.name() == name)
217 }
218
219 #[must_use]
221 pub fn glyphs(self) -> IconGlyphs {
222 pillar_glyphs(match self {
223 Self::Thick => "▌",
224 Self::Thin => "▎",
225 })
226 }
227}
228
229fn pillar_glyphs(glyph: &str) -> IconGlyphs {
231 IconGlyphs { nerd: glyph.to_owned(), unicode: glyph.to_owned(), ascii: " ".to_owned() }
232}
233
234fn parse_pillar(doc: &Doc<'_>, value: &Value<'_>) -> Result<IconGlyphs, Diagnostic> {
237 let text = doc.string(value, "icon `pillar`")?;
238 if let Some(style) = PillarStyle::from_name(text) {
239 return Ok(style.glyphs());
240 }
241 if crate::text::width(text) == 1 && text.chars().count() == 1 {
242 Ok(pillar_glyphs(text))
243 } else {
244 Err(doc.error(
245 &value.span(),
246 format!("icon `pillar` is `{text}`; use \"thick\", \"thin\" or a single one-cell character"),
247 ))
248 }
249}
250
251pub(crate) fn read_icon_table(
254 doc: &Doc<'_>,
255 table: &DeTable<'_>,
256 glyphs: &mut BTreeMap<String, IconGlyphs>,
257 report: &mut Vec<Diagnostic>,
258) {
259 for (key, value) in table {
260 let parsed = if key.get_ref() == PILLAR && value.get_ref().as_str().is_some() {
261 parse_pillar(doc, value)
262 } else {
263 parse_glyphs(doc, key.get_ref(), value, report)
264 .and_then(|glyphs| legacy_glyphs(doc, key.get_ref(), value, glyphs))
265 };
266 match parsed {
267 Ok(parsed) => {
268 glyphs.insert(key.get_ref().to_string(), parsed);
269 }
270 Err(diagnostic) => report.push(diagnostic),
271 }
272 }
273}
274
275const FORMER_KEYS: &[(&str, &str)] = &[("family", "ecosystem")];
279
280fn legacy_glyphs(doc: &Doc<'_>, key: &str, value: &Value<'_>, glyphs: IconGlyphs) -> Result<IconGlyphs, Diagnostic> {
282 if !animation::LEGACY_ICONS.iter().any(|(icon, _)| *icon == key) {
283 return Ok(glyphs);
284 }
285 animation::check_legacy(&glyphs).map_err(|message| doc.error(&value.span(), format!("icon `{key}`: {message}")))?;
286 Ok(glyphs)
287}
288
289fn layer_animations(
292 animations: &mut BTreeMap<String, Arc<CellAnimation>>,
293 glyphs: &BTreeMap<String, IconGlyphs>,
294 own: impl IntoIterator<Item = (String, Arc<CellAnimation>)>,
295) {
296 for (icon, name) in animation::LEGACY_ICONS {
297 if let Some(glyphs) = glyphs.get(icon) {
298 animation::apply_legacy(animations, name, glyphs);
299 }
300 }
301 animations.extend(own);
302}
303
304#[derive(Debug, Clone)]
306struct IconSetSource {
307 name: String,
308 glyphs: BTreeMap<String, IconGlyphs>,
309 animations: BTreeMap<String, Arc<CellAnimation>>,
310}
311
312#[derive(Debug, Clone)]
322pub struct IconSetRegistry {
323 sets: BTreeMap<String, IconSetSource>,
324 added: Vec<String>,
326 diagnostics: Vec<Diagnostic>,
327}
328
329impl IconSetRegistry {
330 #[must_use]
332 pub fn builtin() -> Self {
333 let mut registry = Self { sets: BTreeMap::new(), added: Vec::new(), diagnostics: Vec::new() };
334 for (id, text) in assets::ICON_SETS {
335 registry.add_source(id, &format!("{id}.toml"), text);
336 }
337 registry.added.clear();
338 registry
339 }
340
341 pub fn add_source(&mut self, id: &str, file: &str, text: &str) -> bool {
346 let doc = Doc::new(file, text);
347 let root = match doc.parse() {
348 Ok(root) => root,
349 Err(diagnostic) => {
350 self.diagnostics.push(diagnostic);
351 return false;
352 }
353 };
354 for (key, value) in &root {
355 if !["meta", "icons", "animations"].contains(&key.get_ref().as_ref()) {
356 self.diagnostics.push(doc.error(
357 &value.span(),
358 format!("unknown section `{}`; expected meta, icons and animations", key.get_ref()),
359 ));
360 }
361 }
362 let name = self.read_name(&doc, &root).unwrap_or_else(|| id.to_owned());
363 let mut glyphs = BTreeMap::new();
364 match doc::get(&root, "icons") {
365 Some(icons) => match doc.table(icons, "icons") {
366 Ok(table) => read_icon_table(&doc, table, &mut glyphs, &mut self.diagnostics),
367 Err(diagnostic) => self.diagnostics.push(diagnostic),
368 },
369 None => self.diagnostics.push(Diagnostic::error(None, format!("{file}: missing [icons] table"))),
370 }
371 let mut animations = BTreeMap::new();
372 if let Some(table) = doc::get(&root, "animations") {
373 match doc.table(table, "animations") {
374 Ok(table) => animation::read_animation_table(&doc, table, &mut animations, &mut self.diagnostics),
375 Err(diagnostic) => self.diagnostics.push(diagnostic),
376 }
377 }
378 let animations = animations.into_iter().map(|(name, animation)| (name, Arc::new(animation))).collect();
379 self.sets.insert(id.to_owned(), IconSetSource { name, glyphs, animations });
380 self.added.retain(|added| added != id);
381 self.added.push(id.to_owned());
382 true
383 }
384
385 fn read_name(&mut self, doc: &Doc<'_>, root: &DeTable<'_>) -> Option<String> {
387 let meta = match doc.table(doc::get(root, "meta")?, "meta") {
388 Ok(meta) => meta,
389 Err(diagnostic) => {
390 self.diagnostics.push(diagnostic);
391 return None;
392 }
393 };
394 let mut name = None;
395 for (key, value) in meta {
396 if key.get_ref() != "name" {
397 self.diagnostics.push(doc.error(&value.span(), format!("unknown key `meta.{}`", key.get_ref())));
398 continue;
399 }
400 match doc.string(value, "meta.name") {
401 Ok(text) => name = Some(text.to_owned()),
402 Err(diagnostic) => self.diagnostics.push(diagnostic),
403 }
404 }
405 name
406 }
407
408 pub fn load_dir(&mut self, dir: &Path) -> io::Result<()> {
415 let found = assets::read_toml_dir(dir)?;
416 self.diagnostics.extend(found.skipped);
417 for (id, file, text) in found.files {
418 self.add_source(&id, &file, &text);
419 }
420 Ok(())
421 }
422
423 #[must_use]
425 pub fn list(&self) -> Vec<(String, String)> {
426 self.sets.iter().map(|(id, set)| (id.clone(), set.name.clone())).collect()
427 }
428
429 #[must_use]
431 pub fn diagnostics(&self) -> &[Diagnostic] {
432 &self.diagnostics
433 }
434
435 #[must_use]
439 pub fn icons(&self, id: &str, overrides: &BTreeMap<String, IconGlyphs>, mode: GlyphMode) -> Icons {
440 self.icons_with_animations(id, overrides, &BTreeMap::new(), mode)
441 }
442
443 #[must_use]
451 pub fn icons_with_animations(
452 &self,
453 id: &str,
454 overrides: &BTreeMap<String, IconGlyphs>,
455 animations: &BTreeMap<String, Arc<CellAnimation>>,
456 mode: GlyphMode,
457 ) -> Icons {
458 let fallback = self.sets.get("default");
459 let chosen = self.sets.get(id);
460 let owned = |source: &IconSetSource| source.animations.clone();
461 let mut layered = BTreeMap::new();
462 if let Some(default) = fallback {
463 layer_animations(&mut layered, &default.glyphs, owned(default));
464 }
465 if let Some(set) = chosen.filter(|_| id != "default") {
466 layer_animations(&mut layered, &set.glyphs, owned(set));
467 }
468 layer_animations(&mut layered, overrides, animations.clone());
469 let mut glyphs = self.application_keys();
470 glyphs.extend(chosen.or(fallback).map(|set| set.glyphs.clone()).unwrap_or_default());
471 glyphs.extend(overrides.iter().map(|(k, v)| (k.clone(), v.clone())));
472 glyphs.retain(|key, _| !animation::LEGACY_ICONS.iter().any(|(icon, _)| icon == key));
473 Icons { glyphs, animations: layered, mode }
474 }
475
476 fn application_keys(&self) -> BTreeMap<String, IconGlyphs> {
479 let builtin = self.sets.get("default").map(|set| &set.glyphs);
480 let mut keys = BTreeMap::new();
481 for set in self.added.iter().filter_map(|id| self.sets.get(id)) {
482 let new = set.glyphs.iter().filter(|(key, _)| builtin.is_none_or(|builtin| !builtin.contains_key(*key)));
483 keys.extend(new.map(|(key, glyphs)| (key.clone(), glyphs.clone())));
484 }
485 keys
486 }
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
504pub enum Glyph {
505 Key(String),
507 Literal(String),
509}
510
511impl Glyph {
512 #[must_use]
514 pub fn key(key: impl Into<String>) -> Self {
515 Self::Key(key.into())
516 }
517
518 #[must_use]
522 pub fn literal(glyph: impl Into<String>) -> Self {
523 Self::Literal(glyph.into())
524 }
525
526 #[must_use]
528 pub fn resolve<'a>(&'a self, icons: &'a Icons) -> Cow<'a, str> {
529 match self {
530 Self::Key(key) => icons.glyph(key),
531 Self::Literal(glyph) => Cow::Borrowed(glyph),
532 }
533 }
534}
535
536impl From<&str> for Glyph {
537 fn from(key: &str) -> Self {
538 Self::key(key)
539 }
540}
541
542impl From<String> for Glyph {
543 fn from(key: String) -> Self {
544 Self::Key(key)
545 }
546}
547
548#[derive(Debug, Clone, PartialEq, Eq)]
550pub struct Icons {
551 glyphs: BTreeMap<String, IconGlyphs>,
552 animations: BTreeMap<String, Arc<CellAnimation>>,
553 mode: GlyphMode,
554}
555
556impl Icons {
557 #[must_use]
559 pub fn mode(&self) -> GlyphMode {
560 self.mode
561 }
562
563 pub fn set_mode(&mut self, mode: GlyphMode) {
565 self.mode = mode;
566 }
567
568 fn lookup(&self, key: &str) -> Option<&IconGlyphs> {
570 self.glyphs.get(key).or_else(|| {
571 let (_, now) = FORMER_KEYS.iter().find(|(former, _)| *former == key)?;
572 self.glyphs.get(*now)
573 })
574 }
575
576 #[must_use]
578 pub fn glyph(&self, key: &str) -> Cow<'_, str> {
579 match self.lookup(key) {
580 Some(glyphs) => Cow::Borrowed(glyphs.for_mode(self.mode)),
581 None => Cow::Owned(format!("⟦{key}⟧")),
582 }
583 }
584
585 #[must_use]
587 pub fn frames(&self, key: &str) -> Vec<String> {
588 self.glyph(key).graphemes(true).map(str::to_owned).collect()
589 }
590
591 #[must_use]
593 pub fn glyphs(&self, key: &str) -> Option<&IconGlyphs> {
594 self.lookup(key)
595 }
596
597 #[must_use]
599 pub fn contains(&self, key: &str) -> bool {
600 self.lookup(key).is_some()
601 }
602
603 pub fn keys(&self) -> impl Iterator<Item = &str> {
605 self.glyphs.keys().map(String::as_str)
606 }
607
608 #[must_use]
610 pub fn animation(&self, name: &str) -> Option<&Arc<CellAnimation>> {
611 self.animations.get(name)
612 }
613
614 pub fn animation_names(&self) -> impl Iterator<Item = &str> {
616 self.animations.keys().map(String::as_str)
617 }
618}
619
620#[cfg(test)]
621mod tests;