Skip to main content

fancy_tree/colors/
mod.rs

1//! Provides colors for filepaths.
2use crate::color::Color;
3use crate::ext::PathExt as _;
4use owo_colors::AnsiColors::{Black, Blue, Cyan, Green, Red, Yellow};
5use std::path::Path;
6use std::sync::LazyLock;
7
8/// Gets a color for a path.
9pub fn for_path<P>(path: P) -> Option<Color>
10where
11    P: AsRef<Path>,
12{
13    let path = path.as_ref();
14    path.file_name()
15        .and_then(|s| s.to_str())
16        .and_then(for_filename)
17        .or_else(|| {
18            path.double_extension()
19                .and_then(|(prefix, suffix)| {
20                    prefix
21                        .to_str()
22                        .and_then(|prefix| suffix.to_str().map(|suffix| (prefix, suffix)))
23                })
24                .and_then(for_double_extension)
25        })
26        .or_else(|| {
27            path.extension()
28                .and_then(|extension| extension.to_str())
29                .and_then(for_extension)
30        })
31        .or_else(|| for_filename_glob(path))
32}
33
34/// Gets a color for a filename.
35fn for_filename(filename: &str) -> Option<Color> {
36    // NOTE These should be in alphabetical order and ignoring any leading `.` for
37    //      easier code review.
38    let color = match filename {
39        ".git" | ".gitattributes" | ".gitignore" | ".gitmodules" | ".git-blame-ignore-revs" => {
40            Red.into()
41        }
42        ".github" => Black.into(),
43        "LICENCE" | "LICENSE" | "licence" | "license" => shared::LICENSE,
44        ".vscode" => Blue.into(),
45        _ => return None,
46    };
47    Some(color)
48}
49
50/// Gets a color for a file extension.
51fn for_extension(extension: &str) -> Option<Color> {
52    // NOTE These should be in alphabetical order for easier code review.
53    let color = match extension {
54        "7z" => Black.into(),
55        "gif" => Green.into(),
56        "jpeg" | "jpg" => Yellow.into(),
57        "png" => Cyan.into(),
58        "sqlite" | "sqlite3" => Blue.into(),
59        "tar" => Green.into(),
60        "zip" => Blue.into(),
61        _ => return None,
62    };
63
64    Some(color)
65}
66
67/// Gets a color for the double extension.
68fn for_double_extension(double_extension: (&str, &str)) -> Option<Color> {
69    let color = match double_extension {
70        ("tar", "gz") => Green.into(),
71        _ => return None,
72    };
73
74    Some(color)
75}
76
77/// Gets a color based on a matching glob for a path.
78fn for_filename_glob(path: &Path) -> Option<Color> {
79    use glob::{MatchOptions, Pattern};
80
81    /// Maps a raw glob pattern to a color with `(glob, color)` tuples.
82    const RAW_MAPPINGS: &[(&str, Color)] = &[("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-color mappings.
91    static COMPILED_MAPPINGS: LazyLock<Vec<(Pattern, Color)>> = LazyLock::new(|| {
92        RAW_MAPPINGS
93            .iter()
94            .map(|(raw, color)| (Pattern::new(raw).expect("Pattern should be valid"), *color))
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, color)| glob.matches_with(path, OPTIONS).then_some(*color))
103    })
104}
105
106/// Colors that represent one file type, but have multiple filenames and/or extensions
107/// for that file type.
108mod shared {
109    use super::*;
110
111    /// Color for license files.
112    pub const LICENSE: Color = Color::Ansi(Yellow);
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use rstest::rstest;
119
120    #[rstest]
121    #[case("foo.tar.gz", Some(Green.into()))]
122    fn test_for_path<P>(#[case] path: P, #[case] expected: Option<Color>)
123    where
124        P: AsRef<Path>,
125    {
126        assert_eq!(expected, for_path(path));
127    }
128}