tui_lipan/widgets/file_tree/
fs.rs1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use crate::style::Span;
6use crate::utils::file_icons::{directory_icon, file_icon};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
10pub enum FileIconStyle {
11 #[default]
13 Text,
14 NerdFont,
16 NerdFontColored,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum FileKind {
23 Directory,
25 File,
27 Symlink,
29 Other,
31}
32
33impl FileKind {
34 pub(crate) fn from_file_type(file_type: &fs::FileType) -> Self {
35 if file_type.is_dir() {
36 Self::Directory
37 } else if file_type.is_file() {
38 Self::File
39 } else if file_type.is_symlink() {
40 Self::Symlink
41 } else {
42 Self::Other
43 }
44 }
45
46 pub(crate) fn icon(
47 self,
48 path: &str,
49 expanded: bool,
50 is_root: bool,
51 props: &super::mod_private::FileTreeProps,
52 ) -> Span {
53 let palette = &props.icon_palette;
54
55 match self {
56 Self::Directory => {
57 let path_obj = Path::new(path);
59 if let Some(name) = path_obj.file_name().and_then(|n| n.to_str())
60 && let Some(override_icon) = props.icon_overrides.get(name)
61 {
62 let mut span = Span::new(override_icon.icon.clone());
63 if let Some(color) = override_icon.color {
64 span = span.fg(color);
65 }
66 return span;
67 }
68
69 let base = if expanded {
70 &props.opened_directory_icon
71 } else {
72 &props.directory_icon
73 };
74
75 match props.icon_style {
76 FileIconStyle::Text => Span::new(base.as_ref()),
77 FileIconStyle::NerdFont | FileIconStyle::NerdFontColored => {
78 let (folder_glyph, folder_color) = directory_icon(expanded, palette);
79 let icon = if is_root || !props.show_arrows {
80 folder_glyph.to_string()
81 } else {
82 let arrow = if expanded {
83 "\u{f47c} " } else {
85 "\u{f460} " };
87 format!("{arrow}{folder_glyph}")
88 };
89
90 let mut span = Span::new(icon);
91 if props.icon_style == FileIconStyle::NerdFontColored
92 && let Some(c) = folder_color
93 {
94 span = span.fg(c);
95 }
96 span
97 }
98 }
99 }
100 Self::File => {
101 let path_obj = Path::new(path);
103
104 if let Some(name) = path_obj.file_name().and_then(|n| n.to_str())
106 && let Some(override_icon) = props.icon_overrides.get(name)
107 {
108 let mut span = Span::new(override_icon.icon.clone());
109 if let Some(color) = override_icon.color {
110 span = span.fg(color);
111 }
112 return span;
113 }
114
115 if let Some(ext) = path_obj.extension().and_then(|e| e.to_str())
117 && let Some(override_icon) = props.icon_overrides.get(ext)
118 {
119 let mut span = Span::new(override_icon.icon.clone());
120 if let Some(color) = override_icon.color {
121 span = span.fg(color);
122 }
123 return span;
124 }
125
126 match props.icon_style {
127 FileIconStyle::Text => Span::new(props.file_icon.clone()),
128 FileIconStyle::NerdFont | FileIconStyle::NerdFontColored => {
129 let (icon, color) = file_icon(path, &props.icon_palette);
130 let mut span = Span::new(icon);
131 if props.icon_style == FileIconStyle::NerdFontColored
132 && let Some(c) = color
133 {
134 span = span.fg(c);
135 }
136 span
137 }
138 }
139 }
140 Self::Symlink => {
141 let path_obj = Path::new(path);
143 if let Some(name) = path_obj.file_name().and_then(|n| n.to_str())
144 && let Some(override_icon) = props.icon_overrides.get(name)
145 {
146 let mut span = Span::new(override_icon.icon.clone());
147 if let Some(color) = override_icon.color {
148 span = span.fg(color);
149 }
150 return span;
151 }
152
153 match props.icon_style {
154 FileIconStyle::Text => Span::new(props.symlink_icon.clone()),
155 FileIconStyle::NerdFont | FileIconStyle::NerdFontColored => {
156 let mut span = Span::new("");
157 if props.icon_style == FileIconStyle::NerdFontColored {
158 span = span.fg(palette.cyan);
160 }
161 span
162 }
163 }
164 }
165 Self::Other => {
166 let path_obj = Path::new(path);
168 if let Some(name) = path_obj.file_name().and_then(|n| n.to_str())
169 && let Some(override_icon) = props.icon_overrides.get(name)
170 {
171 let mut span = Span::new(override_icon.icon.clone());
172 if let Some(color) = override_icon.color {
173 span = span.fg(color);
174 }
175 return span;
176 }
177
178 match props.icon_style {
179 FileIconStyle::Text => Span::new(props.other_icon.clone()),
180 FileIconStyle::NerdFont | FileIconStyle::NerdFontColored => {
181 let mut span = Span::new("");
182 if props.icon_style == FileIconStyle::NerdFontColored {
183 span = span.fg(palette.grey);
184 }
185 span
186 }
187 }
188 }
189 }
190 }
191}
192
193pub(crate) fn path_to_display(path: &str) -> String {
194 let home = std::env::var("HOME").unwrap_or_default();
195 if !home.is_empty() && path.starts_with(&home) {
196 return path.replacen(&home, "~", 1);
197 }
198 path.to_string()
199}
200
201#[derive(Clone, Debug)]
202pub(crate) struct FsNode {
203 pub(crate) name: Arc<str>,
204 pub(crate) path: Arc<str>,
205 pub(crate) kind: FileKind,
206 pub(crate) loaded: bool,
207 pub(crate) loading: bool,
208 pub(crate) error: Option<Arc<str>>,
209 pub(crate) children: Vec<FsNode>,
210}
211
212impl FsNode {
213 pub(crate) fn is_dir(&self) -> bool {
214 matches!(self.kind, FileKind::Directory)
215 }
216}
217
218#[derive(Clone, Debug)]
219pub(crate) struct LoadedEntry {
220 pub(crate) name: Arc<str>,
221 pub(crate) path: Arc<str>,
222 pub(crate) kind: FileKind,
223}
224
225#[derive(Clone, Debug)]
226pub(crate) struct DirectoryLoadResult {
227 pub(crate) entries: Vec<LoadedEntry>,
228 pub(crate) omitted: usize,
229 pub(crate) error: Option<Arc<str>>,
230}
231
232pub(crate) fn read_directory(
233 path: &str,
234 show_hidden: bool,
235 max_entries_per_dir: usize,
236) -> DirectoryLoadResult {
237 let mut entries = Vec::new();
238 let mut omitted = 0usize;
239 let root = PathBuf::from(path);
240
241 let read_dir = match fs::read_dir(&root) {
242 Ok(read_dir) => read_dir,
243 Err(err) => {
244 return DirectoryLoadResult {
245 entries,
246 omitted,
247 error: Some(err.to_string().into()),
248 };
249 }
250 };
251
252 for child in read_dir {
253 let Ok(child) = child else {
254 continue;
255 };
256 let name = child.file_name();
257 let Some(name_str) = name.to_str() else {
258 continue;
259 };
260 if !show_hidden && is_hidden_name(name_str) {
261 continue;
262 }
263
264 let Ok(file_type) = child.file_type() else {
265 continue;
266 };
267
268 if entries.len() >= max_entries_per_dir {
269 omitted = omitted.saturating_add(1);
270 continue;
271 }
272
273 let kind = FileKind::from_file_type(&file_type);
274 let child_path = if matches!(kind, FileKind::Symlink) {
278 normalize_path(&child.path())
279 } else {
280 Arc::from(root.join(name_str).to_string_lossy().as_ref())
281 };
282
283 entries.push(LoadedEntry {
284 name: Arc::from(name_str),
285 path: child_path,
286 kind,
287 });
288 }
289
290 entries.sort_by(|left, right| {
291 let left_dir = matches!(left.kind, FileKind::Directory);
292 let right_dir = matches!(right.kind, FileKind::Directory);
293 right_dir
294 .cmp(&left_dir)
295 .then_with(|| left.name.to_lowercase().cmp(&right.name.to_lowercase()))
296 .then_with(|| left.name.cmp(&right.name))
297 });
298
299 DirectoryLoadResult {
300 entries,
301 omitted,
302 error: None,
303 }
304}
305
306pub(crate) fn normalize_path(path: &Path) -> Arc<str> {
307 if let Ok(canonical) = fs::canonicalize(path) {
308 return Arc::<str>::from(canonical.to_string_lossy().as_ref());
309 }
310 Arc::<str>::from(path.to_string_lossy().as_ref())
311}
312
313fn is_hidden_name(name: &str) -> bool {
314 name.starts_with('.') && name != "." && name != ".."
315}
316
317pub(crate) fn root_node(root: &Arc<str>) -> FsNode {
318 let path = PathBuf::from(root.as_ref());
319 let name = display_name(&path);
320
321 match fs::symlink_metadata(&path) {
322 Ok(meta) => {
323 let kind = FileKind::from_file_type(&meta.file_type());
324 FsNode {
325 name,
326 path: normalize_path(&path),
327 kind,
328 loaded: !matches!(kind, FileKind::Directory),
329 loading: false,
330 error: None,
331 children: Vec::new(),
332 }
333 }
334 Err(err) => FsNode {
335 name,
336 path: normalize_path(&path),
337 kind: FileKind::Other,
338 loaded: true,
339 loading: false,
340 error: Some(err.to_string().into()),
341 children: Vec::new(),
342 },
343 }
344}
345
346fn display_name(path: &Path) -> Arc<str> {
347 path.file_name()
348 .and_then(|name| name.to_str())
349 .filter(|name| !name.is_empty())
350 .map(Arc::from)
351 .unwrap_or_else(|| Arc::<str>::from(path.to_string_lossy().as_ref()))
352}