Skip to main content

fancy_tree/icons/
mod.rs

1//! Provides icons for filepaths.
2use crate::ext::PathExt as _;
3use std::path::Path;
4use std::sync::LazyLock;
5
6/// Gets an icon for a path.
7pub fn for_path<P>(path: P) -> Option<&'static str>
8where
9    P: AsRef<Path>,
10{
11    let path = path.as_ref();
12    path.file_name()
13        .and_then(|s| s.to_str())
14        .and_then(for_filename)
15        .or_else(|| {
16            path.double_extension()
17                .and_then(|(prefix, suffix)| {
18                    prefix
19                        .to_str()
20                        .and_then(|prefix| suffix.to_str().map(|suffix| (prefix, suffix)))
21                })
22                .and_then(for_double_extension)
23        })
24        .or_else(|| {
25            path.extension()
26                .and_then(|extension| extension.to_str())
27                .and_then(for_extension)
28        })
29        .or_else(|| for_filename_glob(path))
30}
31
32/// Gets an icon for a filename.
33fn for_filename(filename: &str) -> Option<&'static str> {
34    // NOTE These should be in alphabetical order and ignoring any leading `.` for
35    //      easier code review.
36    let icon = match filename {
37        "CONTRIBUTING.md" => shared::DOC,
38        ".editorconfig" => "\u{e652}", // 
39        ".git" | ".gitattributes" | ".gitignore" | ".gitmodules" | ".git-blame-ignore-revs" => {
40            "\u{e702}"
41        } // 
42        ".github" => "\u{e709}",       // 
43        "LICENCE" | "LICENSE" | "licence" | "license" => shared::LICENSE,
44        "package-lock.json" | "pnpm-lock.yaml" => shared::LOCK,
45        "README" | "README.md" => shared::DOC,
46        ".vscode" => "\u{e8da}", // 
47        _ => return None,
48    };
49    Some(icon)
50}
51
52/// Gets an icon for a file extension.
53fn for_extension(extension: &str) -> Option<&'static str> {
54    // NOTE These should be in alphabetical order for easier code review.
55    let icon = match extension {
56        "7z" | "tar" | "zip" => shared::ARCHIVE,
57        "cfg" => "\u{e615}", // 
58        "gif" | "jpeg" | "jpg" | "png" => shared::IMAGE,
59        "lock" => shared::LOCK,
60        "sqlite" | "sqlite3" => shared::DATABASE,
61        _ => return None,
62    };
63
64    Some(icon)
65}
66
67/// Gets an icon for the double extension.
68fn for_double_extension(double_extension: (&str, &str)) -> Option<&'static str> {
69    let color = match double_extension {
70        ("tar", "gz") => shared::ARCHIVE,
71        _ => return None,
72    };
73
74    Some(color)
75}
76
77/// Gets an icon based on a matching glob for a path.
78fn for_filename_glob(path: &Path) -> Option<&'static str> {
79    use glob::{MatchOptions, Pattern};
80
81    /// Maps a raw glob pattern to an icon with `(glob, icon)` tuples.
82    const RAW_MAPPINGS: &[(&str, &str)] = &[("LICEN[CS]E-*", shared::LICENSE)];
83
84    const OPTIONS: MatchOptions = MatchOptions {
85        case_sensitive: false,
86        require_literal_separator: false,
87        require_literal_leading_dot: false,
88    };
89
90    /// The compiled glob-to-icon mappings.
91    static COMPILED_MAPPINGS: LazyLock<Vec<(Pattern, &'static str)>> = LazyLock::new(|| {
92        RAW_MAPPINGS
93            .iter()
94            .map(|(raw, icon)| (Pattern::new(raw).expect("Pattern should be valid"), *icon))
95            .collect()
96    });
97
98    // NOTE This may receive a path with `./`, so we'll clean to just the prefix.
99    path.file_name().and_then(|s| s.to_str()).and_then(|path| {
100        COMPILED_MAPPINGS
101            .iter()
102            .find_map(|(glob, icon)| glob.matches_with(path, OPTIONS).then_some(*icon))
103    })
104}
105
106/// Icons that represent one file type, but have multiple filenames and/or extensions
107/// for that file type.
108mod shared {
109    /// Icon for archive files, like `.zip` or `.tar.gz`.
110    pub const ARCHIVE: &str = "\u{ea98}"; // 
111    /// Icon for database files.
112    pub const DATABASE: &str = "\u{e706}"; // 
113    /// Icon for documentation files, like READMEs.
114    pub const DOC: &str = "\u{eaa4}"; // 
115    /// Icon for license files.
116    pub const LICENSE: &str = "\u{e60a}"; // 
117    /// Icon for lock files.
118    pub const LOCK: &str = "\u{e672}"; // 
119    /// Icon for image files.
120    pub const IMAGE: &str = "\u{f1c5}"; // 
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use rstest::rstest;
127
128    #[rstest]
129    #[case("example.tar.gz", Some(shared::ARCHIVE))]
130    #[case("example.gif", Some(shared::IMAGE))]
131    #[case("example.jpeg", Some(shared::IMAGE))]
132    #[case("example.jpg", Some(shared::IMAGE))]
133    #[case("example.png", Some(shared::IMAGE))]
134    fn test_for_path<P>(#[case] path: P, #[case] expected: Option<&str>)
135    where
136        P: AsRef<Path>,
137    {
138        assert_eq!(expected, for_path(path));
139    }
140}