1use f00_core::{Entry, EntryKind};
8
9pub fn icon_for(entry: &Entry) -> &'static str {
11 if entry.is_dir_header {
12 return "";
13 }
14
15 match entry.kind {
16 EntryKind::Directory => return icon_for_dir(&entry.name),
17 EntryKind::Symlink => return "\u{f0c1}", EntryKind::Other => return "\u{f15b}", EntryKind::File => {}
20 }
21
22 #[cfg(unix)]
23 if entry.mode & 0o111 != 0 && entry.extension().is_none() {
24 return "\u{f471}"; }
26
27 let base = icon_for_basename(&entry.name);
29 if base != "\u{f15b}" {
30 return base;
31 }
32
33 if let Some(ext) = entry.extension().map(|e| e.to_ascii_lowercase()) {
34 if let Some(ic) = icon_for_extension(&ext) {
35 return ic;
36 }
37 }
38
39 base
40}
41
42fn icon_for_dir(name: &str) -> &'static str {
44 let lower = name.to_ascii_lowercase();
45 let key = lower.trim_end_matches('/');
46
47 match key {
48 "desktop" => "\u{f108}", "documents" | "docs" | "document" => "\u{f15c}", "downloads" | "download" => "\u{f019}", "music" | "audio" | "sounds" => "\u{f001}", "pictures" | "photos" | "images" | "img" | "dcim" => "\u{f1c5}", "videos" | "movies" | "film" | "films" => "\u{f008}", "projects" | "project" | "code" | "src" | "source" | "workspace" | "workspaces" => {
56 "\u{f1b2}" }
58 "public" | "www" | "site" | "sites" => "\u{f0ac}", "templates" | "template" | "skel" => "\u{f1c9}", "notes" | "note" | "notebook" | "notebooks" => "\u{f24a}", "nixos" | "nix" => "\u{f313}", "home" => "\u{f015}", ".config" | "config" | "configuration" | "conf" | "settings" => "\u{f013}",
65 ".git" => "\u{f1d3}",
66 ".github" | ".gitlab" => "\u{f1d3}",
67 "trash" | ".trash" => "\u{f1f8}",
68 "bin" | "sbin" => "\u{f471}",
69 "test" | "tests" | "spec" | "specs" | "__tests__" => "\u{f07c}", "node_modules" | "target" | "build" | "dist" | "out" | "vendor" | "lib" | "libs"
71 | "include" => "\u{f07b}",
72 "applications" | "apps" => "\u{f108}",
73 _ if key.starts_with('.') => "\u{e5fc}", _ => "\u{f07b}", }
76}
77
78fn icon_for_extension(ext: &str) -> Option<&'static str> {
79 Some(match ext {
80 "rs" => "\u{e7a8}",
82 "go" => "\u{e626}",
83 "py" | "pyi" | "pyc" | "pyw" => "\u{e73c}",
84 "js" | "mjs" | "cjs" | "jsx" => "\u{e781}",
85 "ts" | "tsx" | "mts" | "cts" => "\u{e628}",
86 "c" | "h" => "\u{e61e}",
87 "cc" | "cpp" | "cxx" | "hpp" | "hxx" | "hh" => "\u{e61d}",
88 "java" | "jar" | "class" | "kt" | "kts" => "\u{e738}",
89 "rb" | "erb" => "\u{e739}",
90 "php" => "\u{e73d}",
91 "lua" => "\u{e620}",
92 "vim" | "vimrc" => "\u{e62b}",
93 "sh" | "bash" | "zsh" | "fish" | "ps1" | "bat" | "cmd" => "\u{f489}",
94 "html" | "htm" | "xhtml" => "\u{e736}",
96 "css" | "scss" | "sass" | "less" => "\u{e749}",
97 "json" | "jsonc" | "json5" => "\u{e60b}",
98 "toml" => "\u{e615}",
99 "yaml" | "yml" => "\u{e6a8}",
100 "xml" | "xsl" | "xsd" => "\u{f121}",
101 "csv" | "tsv" => "\u{f0ce}",
102 "md" | "markdown" | "mdx" | "rst" | "txt" | "text" | "log" => "\u{f48a}",
103 "png" | "jpg" | "jpeg" | "gif" | "webp" | "ico" | "bmp" | "tiff" | "heic" | "avif" => {
105 "\u{f1c5}"
106 }
107 "svg" | "svgz" => "\u{e698}",
108 "mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" | "wma" | "opus" => "\u{f028}",
109 "mp4" | "mkv" | "webm" | "mov" | "avi" | "m4v" | "wmv" => "\u{f008}",
110 "pdf" => "\u{f1c1}",
112 "epub" | "mobi" => "\u{f02d}",
113 "zip" | "tar" | "gz" | "tgz" | "bz2" | "xz" | "7z" | "rar" | "zst" | "lz4" => "\u{f1c6}",
114 "ttf" | "otf" | "woff" | "woff2" | "eot" => "\u{f031}",
115 "sql" | "db" | "sqlite" | "sqlite3" => "\u{f1c0}",
116 "pem" | "crt" | "cer" | "key" | "pub" | "gpg" | "asc" => "\u{f084}",
117 "lock" => "\u{f023}",
118 "exe" | "dll" | "so" | "dylib" | "o" | "a" | "bin" => "\u{f471}",
119 "dockerignore" => "\u{f308}",
120 _ => return None,
121 })
122}
123
124fn icon_for_basename(name: &str) -> &'static str {
125 match name.to_ascii_lowercase().as_str() {
126 "cargo.toml" | "cargo.lock" => "\u{e7a8}",
127 "go.mod" | "go.sum" => "\u{e626}",
128 "package.json" | "package-lock.json" | "yarn.lock" | "pnpm-lock.yaml" => "\u{e781}",
129 "dockerfile"
130 | "containerfile"
131 | "compose.yaml"
132 | "compose.yml"
133 | "docker-compose.yml"
134 | "docker-compose.yaml" => "\u{f308}",
135 "makefile" | "gnumakefile" | "cmakelists.txt" => "\u{f0ad}",
136 "license" | "license.md" | "license.txt" | "copying" | "copying.txt" => "\u{f2c2}",
137 "readme" | "readme.md" | "readme.txt" | "readme.rst" => "\u{f02d}",
138 "changelog" | "changelog.md" | "changes" | "history.md" => "\u{f48a}",
139 ".gitignore" | ".gitattributes" | ".gitmodules" => "\u{f1d3}",
140 ".editorconfig" | ".prettierrc" | ".eslintrc" | ".eslintrc.js" | ".eslintrc.cjs" => {
141 "\u{e615}"
142 }
143 "flake.nix" | "flake.lock" | "default.nix" | "shell.nix" | "configuration.nix" => {
144 "\u{f313}"
145 }
146 ".bashrc" | ".zshrc" | ".profile" | ".bash_profile" | ".zprofile" => "\u{f489}",
147 "justfile" | "taskfile.yml" | "taskfile.yaml" => "\u{f0ad}",
148 _ => "\u{f15b}",
149 }
150}
151
152pub fn icon_prefix(entry: &Entry, enabled: bool) -> String {
154 if !enabled || entry.is_dir_header {
155 return String::new();
156 }
157 format!("{} ", icon_for(entry))
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use f00_core::{Entry, EntryKind, GitStatus};
164 use std::path::PathBuf;
165
166 fn ent(name: &str, kind: EntryKind) -> Entry {
167 Entry {
168 path: PathBuf::from(name),
169 name: name.into(),
170 kind,
171 size: 0,
172 modified: None,
173 created: None,
174 accessed: None,
175 changed: None,
176 mode: if kind == EntryKind::Directory {
177 0o755
178 } else {
179 0o644
180 },
181 readonly: false,
182 symlink_target: None,
183 depth: 0,
184 git_status: GitStatus::Clean,
185 is_dir_header: false,
186 nlink: 1,
187 uid: 0,
188 gid: 0,
189 inode: 0,
190 blocks: 0,
191 owner: String::new(),
192 group: String::new(),
193 author: String::new(),
194 context: String::new(),
195 }
196 }
197
198 #[test]
199 fn special_dirs_differ_from_generic_folder() {
200 let desk = icon_for(&ent("Desktop", EntryKind::Directory));
201 let dl = icon_for(&ent("Downloads", EntryKind::Directory));
202 let music = icon_for(&ent("Music", EntryKind::Directory));
203 let pics = icon_for(&ent("Pictures", EntryKind::Directory));
204 let vids = icon_for(&ent("Videos", EntryKind::Directory));
205 let projects = icon_for(&ent("Projects", EntryKind::Directory));
206 let gen = icon_for(&ent("random-dir", EntryKind::Directory));
207 assert_ne!(desk, gen);
208 assert_ne!(dl, gen);
209 assert_ne!(desk, dl);
210 assert_ne!(music, pics);
211 assert_ne!(vids, projects);
212 assert_eq!(desk, "\u{f108}");
213 assert_eq!(dl, "\u{f019}");
214 assert_eq!(music, "\u{f001}");
215 assert_eq!(pics, "\u{f1c5}");
216 assert_eq!(vids, "\u{f008}");
217 assert_eq!(projects, "\u{f1b2}");
218 assert_eq!(gen, "\u{f07b}");
219 }
220
221 #[test]
222 fn file_ext_icons() {
223 assert_eq!(icon_for(&ent("main.rs", EntryKind::File)), "\u{e7a8}");
224 assert_eq!(icon_for(&ent("app.py", EntryKind::File)), "\u{e73c}");
225 assert_eq!(icon_for(&ent("pic.png", EntryKind::File)), "\u{f1c5}");
226 assert_eq!(icon_for(&ent("song.mp3", EntryKind::File)), "\u{f028}");
227 assert_eq!(icon_for(&ent("clip.mp4", EntryKind::File)), "\u{f008}");
228 assert_eq!(icon_for(&ent("archive.zip", EntryKind::File)), "\u{f1c6}");
229 }
230
231 #[test]
232 fn basename_icons() {
233 assert_eq!(icon_for(&ent("Cargo.toml", EntryKind::File)), "\u{e7a8}");
234 assert_eq!(icon_for(&ent("Dockerfile", EntryKind::File)), "\u{f308}");
235 assert_eq!(icon_for(&ent("README.md", EntryKind::File)), "\u{f02d}");
236 assert_eq!(icon_for(&ent("flake.nix", EntryKind::File)), "\u{f313}");
237 }
238
239 #[test]
240 fn case_insensitive_dirs() {
241 assert_eq!(
242 icon_for(&ent("MUSIC", EntryKind::Directory)),
243 icon_for(&ent("music", EntryKind::Directory))
244 );
245 }
246
247 #[test]
248 fn icon_prefix_empty_when_disabled() {
249 let e = ent("Desktop", EntryKind::Directory);
250 assert!(icon_prefix(&e, false).is_empty());
251 assert!(icon_prefix(&e, true).starts_with('\u{f108}'));
252 }
253}