1use 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
8pub 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
34fn for_filename(filename: &str) -> Option<Color> {
36 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
50fn for_extension(extension: &str) -> Option<Color> {
52 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
67fn 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
77fn for_filename_glob(path: &Path) -> Option<Color> {
79 use glob::{MatchOptions, Pattern};
80
81 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 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 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
106mod shared {
109 use super::*;
110
111 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}