fm/modes/display/
directory.rs

1use std::borrow::Borrow;
2use std::collections::{BTreeSet, VecDeque};
3use std::fs::read_dir;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use anyhow::{Context, Result};
8
9use crate::app::TabSettings;
10use crate::io::git;
11use crate::modes::{is_not_hidden, path_is_video, FileInfo, FileKind, FilterKind, SortKind, Users};
12use crate::{impl_content, impl_index_to_index, impl_selectable, log_info};
13
14/// Holds the information about file in the current directory.
15/// We know about the current path, the files themselves, the selected index,
16/// the "display all files including hidden" flag and the key to sort files.
17pub struct Directory {
18    /// The current path
19    pub path: Arc<Path>,
20    /// A vector of FileInfo with every file in current path
21    pub content: Vec<FileInfo>,
22    /// The index of the selected file.
23    pub index: usize,
24    used_space: u64,
25}
26
27impl Directory {
28    /// Reads the paths and creates a new `PathContent`.
29    /// Files are sorted by filename by default.
30    /// Selects the first file if any.
31    pub fn new(path: &Path, users: &Users, filter: &FilterKind, show_hidden: bool) -> Result<Self> {
32        let path = Arc::from(path);
33        let mut content = Self::files(&path, show_hidden, filter, users)?;
34        let sort_kind = SortKind::default();
35        sort_kind.sort(&mut content);
36        let index: usize = 0;
37        let used_space = get_used_space(&content);
38
39        Ok(Self {
40            path,
41            content,
42            index,
43            used_space,
44        })
45    }
46
47    pub fn change_directory(
48        &mut self,
49        path: &Path,
50        settings: &TabSettings,
51        users: &Users,
52    ) -> Result<()> {
53        self.content = Self::files(path, settings.show_hidden, &settings.filter, users)?;
54        settings.sort_kind.sort(&mut self.content);
55        self.index = 0;
56        self.used_space = get_used_space(&self.content);
57        self.path = Arc::from(path);
58        Ok(())
59    }
60
61    fn files(
62        path: &Path,
63        show_hidden: bool,
64        filter_kind: &FilterKind,
65        users: &Users,
66    ) -> Result<Vec<FileInfo>> {
67        let mut files: Vec<FileInfo> = Self::create_dot_dotdot(path, users)?;
68        if let Some(true_files) = files_collection(path, users, show_hidden, filter_kind, false) {
69            files.extend(true_files);
70        }
71        Ok(files)
72    }
73
74    fn create_dot_dotdot(path: &Path, users: &Users) -> Result<Vec<FileInfo>> {
75        let current = FileInfo::from_path_with_name(path, ".", users)?;
76        let Some(parent) = path.parent() else {
77            return Ok(vec![current]);
78        };
79        let parent = FileInfo::from_path_with_name(parent, "..", users)?;
80        Ok(vec![current, parent])
81    }
82
83    /// Sort the file with current key.
84    pub fn sort(&mut self, sort_kind: &SortKind) {
85        sort_kind.sort(&mut self.content)
86    }
87
88    /// Calculates the size of the owner column.
89    pub fn owner_column_width(&self) -> usize {
90        let owner_size_btreeset: BTreeSet<usize> =
91            self.iter().map(|file| file.owner.len()).collect();
92        *owner_size_btreeset.iter().next_back().unwrap_or(&1)
93    }
94
95    /// Calculates the size of the group column.
96    pub fn group_column_width(&self) -> usize {
97        let group_size_btreeset: BTreeSet<usize> =
98            self.iter().map(|file| file.group.len()).collect();
99        *group_size_btreeset.iter().next_back().unwrap_or(&1)
100    }
101
102    /// Select the file from a given index.
103    pub fn select_index(&mut self, index: usize) {
104        if index < self.content.len() {
105            self.index = index;
106        }
107    }
108
109    /// Reset the current file content.
110    /// Reads and sort the content with current key.
111    /// Select the first file if any.
112    pub fn reset_files(&mut self, settings: &TabSettings, users: &Users) -> Result<()> {
113        self.content = Self::files(&self.path, settings.show_hidden, &settings.filter, users)?;
114        self.sort(&settings.sort_kind);
115        self.index = 0;
116        Ok(())
117    }
118
119    /// Is the selected file a directory ?
120    /// It may fails if the current path is empty, aka if nothing is selected.
121    pub fn is_selected_dir(&self) -> Result<bool> {
122        let fileinfo = self.selected().context("")?;
123        match fileinfo.file_kind {
124            FileKind::Directory => Ok(true),
125            FileKind::SymbolicLink(true) => {
126                let dest = std::fs::read_link(&fileinfo.path).unwrap_or_default();
127                Ok(dest.is_dir())
128            }
129            _ => Ok(false),
130        }
131    }
132
133    /// Human readable string representation of the space used by _files_
134    /// in current path.
135    /// No recursive exploration of directory.
136    pub fn used_space(&self) -> String {
137        human_size(self.used_space)
138    }
139
140    /// A string representation of the git status of the path.
141    pub fn git_string(&self) -> Result<String> {
142        git(&self.path)
143    }
144
145    /// Returns an iterator of the files (`FileInfo`) in content.
146    #[inline]
147    pub fn iter(&self) -> std::slice::Iter<'_, FileInfo> {
148        self.content.iter()
149    }
150
151    /// Returns an enumeration of the files (`FileInfo`) in content.
152    #[inline]
153    pub fn enumerate(&self) -> Enumerate<std::slice::Iter<'_, FileInfo>> {
154        self.iter().enumerate()
155    }
156
157    /// Returns the correct index jump target to a flagged files.
158    fn find_jump_index(&self, jump_target: &Path) -> Option<usize> {
159        self.content
160            .iter()
161            .position(|file| <Arc<Path> as Borrow<Path>>::borrow(&file.path) == jump_target)
162    }
163
164    /// Select the file from its path. Returns its index in content.
165    pub fn select_file(&mut self, jump_target: &Path) -> usize {
166        let index = self.find_jump_index(jump_target).unwrap_or_default();
167        self.select_index(index);
168        index
169    }
170
171    /// Returns a vector of paths from content
172    pub fn paths(&self) -> Vec<&Path> {
173        self.content
174            .iter()
175            .map(|fileinfo| fileinfo.path.borrow())
176            .collect()
177    }
178
179    /// True iff the selected path is ".." which is the parent dir.
180    pub fn is_dotdot_selected(&self) -> bool {
181        let Some(selected) = &self.selected() else {
182            return false;
183        };
184        let Some(parent) = self.path.parent() else {
185            return false;
186        };
187        selected.path.as_ref() == parent
188    }
189
190    pub fn videos(&self) -> VecDeque<PathBuf> {
191        self.content()
192            .iter()
193            .map(|f| f.path.to_path_buf())
194            .filter(|p| path_is_video(p))
195            .collect()
196    }
197}
198
199impl_index_to_index!(FileInfo, Directory);
200impl_selectable!(Directory);
201impl_content!(Directory, FileInfo);
202
203fn get_used_space(files: &[FileInfo]) -> u64 {
204    files
205        .iter()
206        .filter(|f| !f.is_dir())
207        .map(|f| f.true_size)
208        .sum()
209}
210
211/// Creates an optional vector of fileinfo contained in a file.
212/// Files are filtered by filterkind and the display hidden flag.
213/// Returns None if there's no file.
214pub fn files_collection(
215    path: &Path,
216    users: &Users,
217    show_hidden: bool,
218    filter_kind: &FilterKind,
219    keep_dir: bool,
220) -> Option<Vec<FileInfo>> {
221    match read_dir(path) {
222        Ok(read_dir) => Some(
223            read_dir
224                .filter_map(|direntry| direntry.ok())
225                .filter(|direntry| show_hidden || is_not_hidden(direntry).unwrap_or(true))
226                .filter_map(|direntry| FileInfo::from_direntry(&direntry, users).ok())
227                .filter(|fileinfo| filter_kind.filter_by(fileinfo, keep_dir))
228                .collect(),
229        ),
230        Err(error) => {
231            log_info!("Couldn't read path {path} - {error}", path = path.display(),);
232            None
233        }
234    }
235}
236
237const SIZES: [&str; 9] = ["B", "k", "M", "G", "T", "P", "E", "Z", "Y"];
238
239/// Formats a file size from bytes to human readable string.
240#[inline]
241pub fn human_size(bytes: u64) -> String {
242    let mut factor = 0;
243    let mut size = bytes as f64;
244
245    while size >= 1000.0 && factor < SIZES.len() - 1 {
246        size /= 1000.0;
247        factor += 1;
248    }
249
250    if size < 9.5 && factor > 0 {
251        format!("{size:.1}{unit}", unit = SIZES[factor])
252    } else {
253        format!(
254            "{size:>3}{unit}",
255            size = size.round() as u64,
256            unit = SIZES[factor]
257        )
258    }
259}