1use std::path::Path;
4
5use f00_core::{Entry, EntryKind};
6use lscolors::{LsColors, Style};
7use nu_ansi_term::Color;
8
9#[derive(Clone)]
11pub struct Colorizer {
12 enabled: bool,
13 ls: LsColors,
14}
15
16impl Colorizer {
17 pub fn new(enabled: bool) -> Self {
19 Self {
20 enabled,
21 ls: LsColors::from_env().unwrap_or_default(),
22 }
23 }
24
25 pub fn from_ls_colors(enabled: bool, ls_colors: &str) -> Self {
27 Self {
28 enabled,
29 ls: LsColors::from_string(ls_colors),
30 }
31 }
32
33 pub fn ls_colors(&self) -> &LsColors {
35 &self.ls
36 }
37
38 pub fn enabled(&self) -> bool {
39 self.enabled
40 }
41
42 pub fn paint_name(&self, entry: &Entry, text: &str) -> String {
44 if !self.enabled {
45 return text.to_string();
46 }
47
48 if let Some(style) = self.style_for_entry(entry) {
50 return paint_with_ls_style(text, style);
51 }
52
53 match entry.kind {
55 EntryKind::Directory => Color::Blue.bold().paint(text).to_string(),
56 EntryKind::Symlink => Color::Cyan.paint(text).to_string(),
57 EntryKind::File if entry_is_exec(entry) => Color::Green.bold().paint(text).to_string(),
58 EntryKind::File => text.to_string(),
59 EntryKind::Other => Color::Yellow.paint(text).to_string(),
60 }
61 }
62
63 pub fn style_for_entry<'a>(&'a self, entry: &'a Entry) -> Option<&'a Style> {
65 if let Some(style) = self.ls.style_for_path(&entry.path) {
67 return Some(style);
68 }
69 if let Some(style) = self.ls.style_for_path(Path::new(&entry.name)) {
71 return Some(style);
72 }
73 use lscolors::Indicator;
75 let indicator = match entry.kind {
76 EntryKind::Directory => Some(Indicator::Directory),
77 EntryKind::Symlink => Some(Indicator::SymbolicLink),
78 EntryKind::File if entry_is_exec(entry) => Some(Indicator::ExecutableFile),
79 EntryKind::File => Some(Indicator::RegularFile),
80 EntryKind::Other => {
81 #[cfg(unix)]
82 {
83 match entry.mode & 0o170000 {
84 0o010000 => Some(Indicator::FIFO),
85 0o140000 => Some(Indicator::Socket),
86 0o060000 => Some(Indicator::BlockDevice),
87 0o020000 => Some(Indicator::CharacterDevice),
88 _ => None,
89 }
90 }
91 #[cfg(not(unix))]
92 {
93 None
94 }
95 }
96 };
97 indicator.and_then(|i| self.ls.style_for_indicator(i))
98 }
99
100 pub fn paint_git_char(&self, ch: char) -> String {
101 if !self.enabled {
102 return ch.to_string();
103 }
104 let s = ch.to_string();
105 match ch {
106 'M' => Color::Yellow.bold().paint(s).to_string(),
107 'A' => Color::Green.bold().paint(s).to_string(),
108 'D' | 'U' => Color::Red.bold().paint(s).to_string(),
109 '?' => Color::Purple.paint(s).to_string(),
110 '!' => Color::DarkGray.paint(s).to_string(),
111 'R' => Color::Cyan.bold().paint(s).to_string(),
112 _ => s,
113 }
114 }
115
116 pub fn modern_long_theme(&self, gnu_mode: bool) -> bool {
120 self.enabled && !gnu_mode
121 }
122
123 pub fn paint_perms(&self, perms: &str, gnu_mode: bool) -> String {
125 if !self.modern_long_theme(gnu_mode) || perms.is_empty() {
126 return perms.to_string();
127 }
128 let mut out = String::with_capacity(perms.len() * 8);
129 for (i, ch) in perms.chars().enumerate() {
130 let painted = if i == 0 {
131 match ch {
132 'd' => Color::Blue.bold().paint(ch.to_string()).to_string(),
133 'l' => Color::Cyan.bold().paint(ch.to_string()).to_string(),
134 'c' | 'b' => Color::Yellow.bold().paint(ch.to_string()).to_string(),
135 'p' | 's' => Color::Purple.paint(ch.to_string()).to_string(),
136 _ => Color::DarkGray.paint(ch.to_string()).to_string(),
137 }
138 } else {
139 match ch {
140 'r' => Color::Yellow.paint(ch.to_string()).to_string(),
141 'w' => Color::Red.paint(ch.to_string()).to_string(),
142 'x' | 's' | 't' | 'S' | 'T' => {
143 Color::Green.bold().paint(ch.to_string()).to_string()
144 }
145 '-' => Color::DarkGray.paint(ch.to_string()).to_string(),
146 _ => ch.to_string(),
147 }
148 };
149 out.push_str(&painted);
150 }
151 out
152 }
153
154 pub fn paint_meta(&self, text: &str, gnu_mode: bool) -> String {
156 if !self.modern_long_theme(gnu_mode) {
157 return text.to_string();
158 }
159 Color::DarkGray.paint(text).to_string()
160 }
161
162 pub fn paint_user(&self, text: &str, gnu_mode: bool) -> String {
164 if !self.modern_long_theme(gnu_mode) {
165 return text.to_string();
166 }
167 Color::Yellow.paint(text).to_string()
168 }
169
170 pub fn paint_size(&self, text: &str, bytes: u64, gnu_mode: bool) -> String {
172 if !self.modern_long_theme(gnu_mode) {
173 return text.to_string();
174 }
175 let style = if bytes >= 1_073_741_824 {
176 Color::Red.bold()
178 } else if bytes >= 10_485_760 {
179 Color::Yellow.bold()
181 } else if bytes >= 1_048_576 {
182 Color::Green.bold()
184 } else if bytes > 0 {
185 Color::Green.normal()
186 } else {
187 Color::DarkGray.normal()
188 };
189 style.paint(text).to_string()
190 }
191
192 pub fn paint_time(&self, text: &str, gnu_mode: bool) -> String {
194 if !self.modern_long_theme(gnu_mode) {
195 return text.to_string();
196 }
197 Color::Blue.paint(text).to_string()
198 }
199
200 pub fn paint_symlink_name(
202 &self,
203 entry: &Entry,
204 icon_and_name: &str,
205 arrow_and_target: &str,
206 gnu_mode: bool,
207 ) -> String {
208 if !self.modern_long_theme(gnu_mode) {
209 let full = format!("{icon_and_name}{arrow_and_target}");
210 return self.paint_name(entry, &full);
211 }
212 let name = self.paint_name(entry, icon_and_name);
213 if arrow_and_target.is_empty() {
214 return name;
215 }
216 let target = if let Some(rest) = arrow_and_target.strip_prefix(" -> ") {
218 format!(
219 " {} {}",
220 Color::DarkGray.paint("→"),
221 Color::Cyan.dimmed().paint(rest)
222 )
223 } else {
224 Color::DarkGray.paint(arrow_and_target).to_string()
225 };
226 format!("{name}{target}")
227 }
228}
229
230fn paint_with_ls_style(text: &str, style: &Style) -> String {
231 style.to_nu_ansi_term_style().paint(text).to_string()
232}
233
234fn entry_is_exec(entry: &Entry) -> bool {
235 #[cfg(unix)]
236 {
237 entry.mode & 0o111 != 0
238 }
239 #[cfg(not(unix))]
240 {
241 let _ = entry;
242 false
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use f00_core::{Entry, EntryKind, GitStatus};
250 use std::path::PathBuf;
251
252 fn file(name: &str) -> Entry {
253 Entry {
254 path: PathBuf::from(name),
255 name: name.into(),
256 kind: EntryKind::File,
257 size: 0,
258 modified: None,
259 created: None,
260 accessed: None,
261 changed: None,
262 mode: 0o644,
263 readonly: false,
264 symlink_target: None,
265 depth: 0,
266 git_status: GitStatus::Clean,
267 is_dir_header: false,
268 nlink: 1,
269 uid: 0,
270 gid: 0,
271 inode: 0,
272 blocks: 0,
273 owner: "u".into(),
274 group: "g".into(),
275 author: "u".into(),
276 context: String::new(),
277 }
278 }
279
280 #[test]
281 fn disabled_no_ansi() {
282 let c = Colorizer::from_ls_colors(false, "*.rs=01;31");
283 let e = file("main.rs");
284 assert_eq!(c.paint_name(&e, "main.rs"), "main.rs");
285 }
286
287 #[test]
288 fn ls_colors_extension() {
289 let c = Colorizer::from_ls_colors(true, "*.rs=01;31:");
290 let e = file("main.rs");
291 let painted = c.paint_name(&e, "main.rs");
292 assert!(painted.contains("main.rs"), "{painted:?}");
294 let _ = c.style_for_entry(&e);
296 }
297
298 #[test]
299 fn modern_theme_off_under_gnu() {
300 let c = Colorizer::from_ls_colors(true, "");
301 assert!(!c.modern_long_theme(true));
302 assert!(c.modern_long_theme(false));
303 assert_eq!(c.paint_perms("-rwxr-xr-x", true), "-rwxr-xr-x");
304 assert_ne!(c.paint_perms("-rwxr-xr-x", false), "-rwxr-xr-x");
305 }
306
307 #[test]
308 fn modern_size_tints() {
309 let c = Colorizer::from_ls_colors(true, "");
310 let small = c.paint_size("1.0K", 1024, false);
311 let big = c.paint_size("2.0G", 2_147_483_648, false);
312 assert!(small.contains("1.0K"), "{small}");
313 assert!(big.contains("2.0G"), "{big}");
314 assert!(small.contains('\u{1b}') || small != "1.0K");
316 assert!(big.contains('\u{1b}') || big != "2.0G");
317 }
318}