Skip to main content

ratatui_explorer/file_explorer/
builder.rs

1use std::{io::Result, path::PathBuf, sync::Arc};
2
3use super::{File, FileExplorer, Filter};
4use crate::Theme;
5
6/// Builder for creating a [`FileExplorer`](FileExplorer).
7///
8/// By default, the builder create `FileExplorer` with a working directory set to the current one.
9#[derive(Clone, Default, educe::Educe)]
10#[educe(Debug, PartialEq, Eq, Hash)]
11pub struct FileExplorerBuilder {
12    cwd: Option<PathBuf>,
13    theme: Option<Theme>,
14    show_hidden: bool,
15    #[educe(Debug(ignore), PartialEq(ignore), Hash(ignore))]
16    filter: Option<Arc<Filter>>,
17    custom_selected: bool,
18}
19
20impl FileExplorerBuilder {
21    /// Set the current working directory for the `FileExplorer`.
22    /// If not set, it defaults to the current directory.
23    ///
24    /// # Examples
25    /// Suppose you have this tree file:
26    /// ```plaintext
27    /// /
28    /// ├── .git
29    /// └── Documents
30    ///     ├── passport.png
31    ///     └── resume.pdf
32    /// ```
33    /// You can create a new `FileExplorer` like this:
34    /// ```no_run
35    /// # use ratatui_explorer::FileExplorerBuilder;
36    /// let file_explorer = FileExplorerBuilder::default()
37    ///     .working_dir("/Documents")
38    ///     .build()
39    ///     .unwrap();
40    ///
41    /// assert_eq!(file_explorer.cwd().display().to_string(), "/Documents");
42    /// ```
43    pub fn working_dir<P: Into<PathBuf>>(mut self, working_dir: P) -> Self {
44        self.cwd = Some(working_dir.into());
45        self
46    }
47
48    /// Same as [`working_dir`](FileExplorerBuilder::working_dir) but will pre-select the file in the working directory.
49    ///
50    /// This method set the working directory to the parent directory of the provided file and select the file in the file explorer.
51    /// You can also select a directory (eg. select `/Documents` inside `/`).
52    ///
53    /// # Examples
54    /// Suppose you have this tree file:
55    /// ```plaintext
56    /// /
57    /// ├── .git
58    /// └── Documents
59    ///     ├── passport.png
60    ///     └── resume.pdf
61    /// ```
62    /// You can create a new `FileExplorer` selecting `passport.png` like this:
63    /// ```no_run
64    /// # use ratatui_explorer::FileExplorerBuilder;
65    /// let file_explorer = FileExplorerBuilder::default()
66    ///     .working_file("/Documents/passport.png")
67    ///     .build()
68    ///     .unwrap();
69    ///
70    /// assert_eq!(file_explorer.cwd().display().to_string(), "/Documents");
71    /// assert_eq!(file_explorer.current().path.display().to_string(), "/Documents/passport.png");
72    /// ```
73    pub fn working_file<P: Into<PathBuf>>(mut self, working_file: P) -> Self {
74        self.custom_selected = true;
75        self.working_dir(working_file)
76    }
77
78    /// Set whether to show hidden files in the `FileExplorer`. Defaults to `false`.
79    pub fn show_hidden(mut self, show: bool) -> Self {
80        self.show_hidden = show;
81        self
82    }
83
84    /// Set a filter and map for the `FileExplorer`.
85    ///
86    /// If not set, all files are shown. Hidden files are filtered **before** this
87    /// filter will be apply.
88    ///
89    /// # Examples
90    ///
91    /// ```no_run
92    /// # use ratatui_explorer::FileExplorerBuilder;
93    /// let file_explorer = FileExplorerBuilder::default()
94    ///     .filter_map(|file| if file.is_dir { Some(file) } else { None })
95    ///     .build()
96    ///     .unwrap();
97    ///
98    /// /* Only directories are shown */
99    /// ```
100    pub fn filter_map(mut self, f: impl Fn(File) -> Option<File> + Send + Sync + 'static) -> Self {
101        self.filter = Some(Arc::new(f));
102        self
103    }
104
105    /// Set the theme for the `FileExplorer`.
106    /// If not set, it defaults to [`Theme::new`](Theme::new).
107    ///
108    /// # Examples
109    ///
110    /// ```no_run
111    /// # use ratatui_explorer::{FileExplorerBuilder, Theme};
112    /// let file_explorer = FileExplorerBuilder::default()
113    ///     .theme(Theme::default().add_default_title())
114    ///     .build()
115    ///     .unwrap();
116    /// ```
117    pub fn theme(mut self, theme: Theme) -> Self {
118        self.theme = Some(theme);
119        self
120    }
121
122    /// Build the `FileExplorer` instance based on the provided configuration.
123    ///
124    /// # Errors
125    ///
126    /// Will return `Err` if the setted working directory can not be listed.
127    ///
128    /// Will return `Err` if **NO** working directory have been setted and current working directory can not be listed.
129    /// See [`current_dir`](https://doc.rust-lang.org/stable/std/env/fn.current_dir.html) for more information.
130    ///
131    #[allow(clippy::unwrap_or_default)]
132    pub fn build(self) -> Result<FileExplorer> {
133        let show_hidden = self.show_hidden;
134        let theme = self.theme.unwrap_or_else(Theme::new);
135        let filter = self.filter;
136
137        let mut file_explorer = FileExplorer {
138            cwd: PathBuf::new(),
139            files: Vec::new(),
140            show_hidden,
141            selected: 0,
142            theme,
143            filter,
144        };
145
146        if self.custom_selected {
147            file_explorer.set_working_file(self.cwd.unwrap())?;
148        } else {
149            file_explorer.set_cwd(self.cwd.clone().unwrap_or(std::env::current_dir()?))?;
150        }
151
152        Ok(file_explorer)
153    }
154
155    /// Shortcut method to create a `FileExplorer` with a custom theme.
156    /// See [`theme`](FileExplorerBuilder::theme) for more information about the theme configuration.
157    pub fn build_with_theme(theme: Theme) -> Result<FileExplorer> {
158        Self::default().theme(theme).build()
159    }
160
161    /// Shortcut method to create a `FileExplorer` with a custom working directory.
162    /// See [`working_dir`](FileExplorerBuilder::working_dir) for more information about the working directory configuration.
163    pub fn build_with_working_dir<P: Into<PathBuf>>(working_dir: P) -> Result<FileExplorer> {
164        Self::default().working_dir(working_dir).build()
165    }
166
167    /// Shortcut method to create a `FileExplorer` with a custom working directory and file.
168    /// See [`working_file`](FileExplorerBuilder::working_file) for more information about the working directory configuration.
169    pub fn build_with_working_file<P: Into<PathBuf>>(working_dir: P) -> Result<FileExplorer> {
170        Self::default().working_file(working_dir).build()
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    use std::fs::{self, File};
179    use tempfile::TempDir;
180
181    /// Build this temporary file system:
182    /// ```plaintext
183    /// <unknow>
184    /// └ root
185    ///   ├── .git
186    ///   └── Documents
187    ///       ├── passport.png
188    ///       └── resume.pdf
189    /// ```
190    fn build_tmp_file_system() -> Result<TempDir> {
191        let root = TempDir::new()?;
192
193        let git_path = root.path().join(".git");
194        let documents_path = root.path().join("Documents");
195        let passport_path = root.path().join("Documents/passport.png");
196        let resume_path = root.path().join("Documents/resume.pdf");
197
198        fs::create_dir(git_path)?;
199        fs::create_dir(documents_path)?;
200        File::create(passport_path)?;
201        File::create(resume_path)?;
202
203        Ok(root)
204    }
205
206    #[test]
207    fn test_thread_safe() {
208        fn is_sync<T: Sync>() {}
209
210        fn is_send<T: Send>() {}
211
212        is_send::<FileExplorerBuilder>();
213        is_sync::<FileExplorerBuilder>();
214    }
215
216    #[test]
217    fn test_working_file_correcty_set_selected_file() -> Result<()> {
218        let root = build_tmp_file_system()?;
219        let documents_path = root.path().join("Documents");
220        let passport_path = documents_path.join("passport.png");
221
222        let file_explorer = FileExplorerBuilder::default()
223            .working_file(&passport_path)
224            .build()
225            .unwrap();
226
227        assert_eq!(*file_explorer.cwd(), documents_path);
228        assert_eq!(file_explorer.current().path, passport_path);
229
230        Ok(())
231    }
232
233    #[test]
234    fn test_working_file_correcty_set_selected_dir() -> Result<()> {
235        let root = build_tmp_file_system()?;
236        let documents_path = root.path().join("Documents");
237
238        let file_explorer = FileExplorerBuilder::default()
239            .show_hidden(true)
240            .working_file(&documents_path)
241            .build()
242            .unwrap();
243
244        assert_eq!(*file_explorer.cwd(), root.path());
245        assert_eq!(*file_explorer.current().path, documents_path);
246
247        Ok(())
248    }
249}