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::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_icon = if expanded { " " } else { " " };
79 let icon = if is_root || !props.show_arrows {
80 folder_icon.chars().skip(2).collect::<String>()
82 } else {
83 folder_icon.to_string()
84 };
85
86 let mut span = Span::new(icon);
87 if props.icon_style == FileIconStyle::NerdFontColored {
88 span = span.fg(palette.blue);
90 }
91 span
92 }
93 }
94 }
95 Self::File => {
96 let path_obj = Path::new(path);
98
99 if let Some(name) = path_obj.file_name().and_then(|n| n.to_str())
101 && let Some(override_icon) = props.icon_overrides.get(name)
102 {
103 let mut span = Span::new(override_icon.icon.clone());
104 if let Some(color) = override_icon.color {
105 span = span.fg(color);
106 }
107 return span;
108 }
109
110 if let Some(ext) = path_obj.extension().and_then(|e| e.to_str())
112 && let Some(override_icon) = props.icon_overrides.get(ext)
113 {
114 let mut span = Span::new(override_icon.icon.clone());
115 if let Some(color) = override_icon.color {
116 span = span.fg(color);
117 }
118 return span;
119 }
120
121 match props.icon_style {
122 FileIconStyle::Text => Span::new(props.file_icon.clone()),
123 FileIconStyle::NerdFont | FileIconStyle::NerdFontColored => {
124 let (icon, color) = file_icon(path, &props.icon_palette);
125 let mut span = Span::new(icon);
126 if props.icon_style == FileIconStyle::NerdFontColored
127 && let Some(c) = color
128 {
129 span = span.fg(c);
130 }
131 span
132 }
133 }
134 }
135 Self::Symlink => {
136 let path_obj = Path::new(path);
138 if let Some(name) = path_obj.file_name().and_then(|n| n.to_str())
139 && let Some(override_icon) = props.icon_overrides.get(name)
140 {
141 let mut span = Span::new(override_icon.icon.clone());
142 if let Some(color) = override_icon.color {
143 span = span.fg(color);
144 }
145 return span;
146 }
147
148 match props.icon_style {
149 FileIconStyle::Text => Span::new(props.symlink_icon.clone()),
150 FileIconStyle::NerdFont | FileIconStyle::NerdFontColored => {
151 let mut span = Span::new("");
152 if props.icon_style == FileIconStyle::NerdFontColored {
153 span = span.fg(palette.cyan);
155 }
156 span
157 }
158 }
159 }
160 Self::Other => {
161 let path_obj = Path::new(path);
163 if let Some(name) = path_obj.file_name().and_then(|n| n.to_str())
164 && let Some(override_icon) = props.icon_overrides.get(name)
165 {
166 let mut span = Span::new(override_icon.icon.clone());
167 if let Some(color) = override_icon.color {
168 span = span.fg(color);
169 }
170 return span;
171 }
172
173 match props.icon_style {
174 FileIconStyle::Text => Span::new(props.other_icon.clone()),
175 FileIconStyle::NerdFont | FileIconStyle::NerdFontColored => {
176 let mut span = Span::new("");
177 if props.icon_style == FileIconStyle::NerdFontColored {
178 span = span.fg(palette.grey);
179 }
180 span
181 }
182 }
183 }
184 }
185 }
186}
187
188pub(crate) fn path_to_display(path: &str) -> String {
189 let home = std::env::var("HOME").unwrap_or_default();
190 if !home.is_empty() && path.starts_with(&home) {
191 return path.replacen(&home, "~", 1);
192 }
193 path.to_string()
194}
195
196#[derive(Clone, Debug)]
197pub(crate) struct FsNode {
198 pub(crate) name: Arc<str>,
199 pub(crate) path: Arc<str>,
200 pub(crate) kind: FileKind,
201 pub(crate) loaded: bool,
202 pub(crate) loading: bool,
203 pub(crate) error: Option<Arc<str>>,
204 pub(crate) children: Vec<FsNode>,
205}
206
207impl FsNode {
208 pub(crate) fn is_dir(&self) -> bool {
209 matches!(self.kind, FileKind::Directory)
210 }
211}
212
213#[derive(Clone, Debug)]
214pub(crate) struct LoadedEntry {
215 pub(crate) name: Arc<str>,
216 pub(crate) path: Arc<str>,
217 pub(crate) kind: FileKind,
218}
219
220#[derive(Clone, Debug)]
221pub(crate) struct DirectoryLoadResult {
222 pub(crate) entries: Vec<LoadedEntry>,
223 pub(crate) omitted: usize,
224 pub(crate) error: Option<Arc<str>>,
225}
226
227pub(crate) fn read_directory(
228 path: &str,
229 show_hidden: bool,
230 max_entries_per_dir: usize,
231) -> DirectoryLoadResult {
232 let mut entries = Vec::new();
233 let mut omitted = 0usize;
234 let root = PathBuf::from(path);
235
236 let read_dir = match fs::read_dir(&root) {
237 Ok(read_dir) => read_dir,
238 Err(err) => {
239 return DirectoryLoadResult {
240 entries,
241 omitted,
242 error: Some(err.to_string().into()),
243 };
244 }
245 };
246
247 for child in read_dir {
248 let Ok(child) = child else {
249 continue;
250 };
251 let name = child.file_name();
252 let Some(name_str) = name.to_str() else {
253 continue;
254 };
255 if !show_hidden && is_hidden_name(name_str) {
256 continue;
257 }
258
259 let Ok(file_type) = child.file_type() else {
260 continue;
261 };
262
263 if entries.len() >= max_entries_per_dir {
264 omitted = omitted.saturating_add(1);
265 continue;
266 }
267
268 let kind = FileKind::from_file_type(&file_type);
269 let child_path = if matches!(kind, FileKind::Symlink) {
273 normalize_path(&child.path())
274 } else {
275 Arc::from(root.join(name_str).to_string_lossy().as_ref())
276 };
277
278 entries.push(LoadedEntry {
279 name: Arc::from(name_str),
280 path: child_path,
281 kind,
282 });
283 }
284
285 entries.sort_by(|left, right| {
286 let left_dir = matches!(left.kind, FileKind::Directory);
287 let right_dir = matches!(right.kind, FileKind::Directory);
288 right_dir
289 .cmp(&left_dir)
290 .then_with(|| left.name.to_lowercase().cmp(&right.name.to_lowercase()))
291 .then_with(|| left.name.cmp(&right.name))
292 });
293
294 DirectoryLoadResult {
295 entries,
296 omitted,
297 error: None,
298 }
299}
300
301pub(crate) fn normalize_path(path: &Path) -> Arc<str> {
302 if let Ok(canonical) = fs::canonicalize(path) {
303 return Arc::<str>::from(canonical.to_string_lossy().as_ref());
304 }
305 Arc::<str>::from(path.to_string_lossy().as_ref())
306}
307
308fn is_hidden_name(name: &str) -> bool {
309 name.starts_with('.') && name != "." && name != ".."
310}
311
312pub(crate) fn root_node(root: &Arc<str>) -> FsNode {
313 let path = PathBuf::from(root.as_ref());
314 let name = display_name(&path);
315
316 match fs::symlink_metadata(&path) {
317 Ok(meta) => {
318 let kind = FileKind::from_file_type(&meta.file_type());
319 FsNode {
320 name,
321 path: normalize_path(&path),
322 kind,
323 loaded: !matches!(kind, FileKind::Directory),
324 loading: false,
325 error: None,
326 children: Vec::new(),
327 }
328 }
329 Err(err) => FsNode {
330 name,
331 path: normalize_path(&path),
332 kind: FileKind::Other,
333 loaded: true,
334 loading: false,
335 error: Some(err.to_string().into()),
336 children: Vec::new(),
337 },
338 }
339}
340
341fn display_name(path: &Path) -> Arc<str> {
342 path.file_name()
343 .and_then(|name| name.to_str())
344 .filter(|name| !name.is_empty())
345 .map(Arc::from)
346 .unwrap_or_else(|| Arc::<str>::from(path.to_string_lossy().as_ref()))
347}