1use crate::ext::PathExt as _;
3use std::path::Path;
4use std::sync::LazyLock;
5
6pub 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
32fn for_filename(filename: &str) -> Option<&'static str> {
34 let icon = match filename {
37 "CONTRIBUTING.md" => shared::DOC,
38 ".editorconfig" => "\u{e652}", ".git" | ".gitattributes" | ".gitignore" | ".gitmodules" | ".git-blame-ignore-revs" => {
40 "\u{e702}"
41 } ".github" => "\u{e709}", "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}", _ => return None,
48 };
49 Some(icon)
50}
51
52fn for_extension(extension: &str) -> Option<&'static str> {
54 let icon = match extension {
56 "7z" | "tar" | "zip" => shared::ARCHIVE,
57 "cfg" => "\u{e615}", "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
67fn 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
77fn for_filename_glob(path: &Path) -> Option<&'static str> {
79 use glob::{MatchOptions, Pattern};
80
81 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 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 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
106mod shared {
109 pub const ARCHIVE: &str = "\u{ea98}"; pub const DATABASE: &str = "\u{e706}"; pub const DOC: &str = "\u{eaa4}"; pub const LICENSE: &str = "\u{e60a}"; pub const LOCK: &str = "\u{e672}"; pub const IMAGE: &str = "\u{f1c5}"; }
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}