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}