Skip to main content

fm/modes/menu/
bulkrename.rs

1use std::io::{BufRead, Write};
2use std::path::{Path, PathBuf};
3use std::sync::{mpsc::Sender, Arc};
4use std::thread;
5use std::time::{Duration, SystemTime};
6
7use anyhow::{anyhow, Result};
8
9use crate::common::{random_name, rename_filename, TMP_FOLDER_PATH};
10use crate::event::FmEvents;
11use crate::{log_info, log_line};
12
13type OptionVecPathBuf = Option<Vec<PathBuf>>;
14type OptionVecPairPathBuf = Option<Vec<(PathBuf, PathBuf)>>;
15
16struct BulkExecutor {
17    index: usize,
18    original_filepath: Vec<PathBuf>,
19    temp_file: PathBuf,
20    new_filenames: Vec<String>,
21    parent_dir: String,
22}
23
24impl BulkExecutor {
25    fn new(original_filepath: Vec<PathBuf>, parent_dir: &str) -> Self {
26        let temp_file = generate_random_filepath();
27        Self {
28            index: 0,
29            original_filepath,
30            temp_file,
31            new_filenames: vec![],
32            parent_dir: parent_dir.to_owned(),
33        }
34    }
35
36    fn ask_filenames(self) -> Result<Self> {
37        create_random_file(&self.temp_file)?;
38        log_info!("created {temp_file}", temp_file = self.temp_file.display());
39        self.write_original_names()?;
40        // open_temp_file_with_editor(&self.temp_file, status)?;
41
42        // self.watch_modification_in_thread(original_modification, fm_sender);
43        Ok(self)
44    }
45
46    fn watch_modification_in_thread(&self, fm_sender: Arc<Sender<FmEvents>>) -> Result<()> {
47        let original_modification = get_modified_date(&self.temp_file)?;
48        let filepath = self.temp_file.to_owned();
49        thread::spawn(move || {
50            loop {
51                if is_file_modified(&filepath, original_modification).unwrap_or(true) {
52                    break;
53                }
54                thread::sleep(Duration::from_millis(100));
55            }
56            fm_sender.send(FmEvents::BulkExecute).unwrap_or_default();
57        });
58        Ok(())
59    }
60    fn get_new_names(&mut self) -> Result<()> {
61        self.new_filenames = get_new_filenames(&self.temp_file)?;
62        Ok(())
63    }
64
65    fn write_original_names(&self) -> Result<()> {
66        let mut file = std::fs::File::create(&self.temp_file)?;
67        log_info!("created {temp_file}", temp_file = self.temp_file.display());
68
69        for path in &self.original_filepath {
70            let Some(os_filename) = path.file_name() else {
71                return Ok(());
72            };
73            let Some(filename) = os_filename.to_str() else {
74                return Ok(());
75            };
76            file.write_all(filename.as_bytes())?;
77            file.write_all(b"\n")?;
78        }
79        Ok(())
80    }
81
82    fn execute(&self) -> Result<(OptionVecPairPathBuf, OptionVecPathBuf)> {
83        let paths = self.rename_create();
84        self.del_temporary_file()?;
85        paths
86    }
87
88    fn rename_create(&self) -> Result<(OptionVecPairPathBuf, OptionVecPathBuf)> {
89        let updated_paths = self.rename_all(&self.new_filenames)?;
90        let created_paths = self.create_all_files(&self.new_filenames)?;
91        Ok((updated_paths, created_paths))
92    }
93
94    fn rename_all(&self, new_filenames: &[String]) -> Result<OptionVecPairPathBuf> {
95        let mut updated_paths = vec![];
96        for (old_path, filename) in self.original_filepath.iter().zip(new_filenames.iter()) {
97            match rename_filename(old_path, filename) {
98                Ok(new_path) => updated_paths.push((old_path.to_owned(), new_path)),
99                Err(error) => log_info!(
100                    "Error renaming {old_path} to {filename}. Error: {error:?}",
101                    old_path = old_path.display()
102                ),
103            }
104        }
105        log_line!("Bulk renamed {len} files", len = updated_paths.len());
106        Ok(Some(updated_paths))
107    }
108
109    fn create_all_files(&self, new_filenames: &[String]) -> Result<OptionVecPathBuf> {
110        let mut paths = vec![];
111        for filename in new_filenames.iter().skip(self.original_filepath.len()) {
112            let Some(path) = self.create_file(filename)? else {
113                continue;
114            };
115            paths.push(path)
116        }
117        log_line!("Bulk created {len} files", len = paths.len());
118        Ok(Some(paths))
119    }
120
121    fn create_file(&self, filename: &str) -> Result<Option<PathBuf>> {
122        let mut new_path = std::path::PathBuf::from(&self.parent_dir);
123        if !filename.ends_with('/') {
124            new_path.push(filename);
125            let Some(parent) = new_path.parent() else {
126                return Ok(None);
127            };
128            log_info!("Bulk new files. Creating parent: {}", parent.display());
129            if std::fs::create_dir_all(parent).is_err() {
130                return Ok(None);
131            };
132            log_info!("creating: {new_path:?}");
133            std::fs::File::create(&new_path)?;
134            log_line!("Bulk created {new_path}", new_path = new_path.display());
135        } else {
136            new_path.push(filename);
137            log_info!("Bulk creating dir: {}", new_path.display());
138            std::fs::create_dir_all(&new_path)?;
139            log_line!("Bulk created {new_path}", new_path = new_path.display());
140        }
141        Ok(Some(new_path))
142    }
143
144    fn del_temporary_file(&self) -> Result<()> {
145        std::fs::remove_file(&self.temp_file)?;
146        Ok(())
147    }
148
149    fn len(&self) -> usize {
150        self.new_filenames.len()
151    }
152
153    fn next(&mut self) {
154        self.index = (self.index + 1) % self.len()
155    }
156
157    fn prev(&mut self) {
158        if self.index > 0 {
159            self.index -= 1
160        } else {
161            self.index = self.len() - 1
162        }
163    }
164
165    /// Set the index to a new value if the value is below the length.
166    fn set_index(&mut self, index: usize) {
167        if index < self.len() {
168            self.index = index;
169        }
170    }
171
172    /// Reverse the received effect if the index match the selected index.
173    fn style(&self, index: usize, style: &ratatui::style::Style) -> ratatui::style::Style {
174        let mut style = *style;
175        if index == self.index {
176            style.add_modifier |= ratatui::style::Modifier::REVERSED;
177        }
178        style
179    }
180}
181
182fn generate_random_filepath() -> PathBuf {
183    let mut filepath = PathBuf::from(&TMP_FOLDER_PATH);
184    filepath.push(random_name());
185    filepath
186}
187
188fn create_random_file(temp_file: &Path) -> Result<()> {
189    std::fs::File::create(temp_file)?;
190    Ok(())
191}
192
193fn get_modified_date(filepath: &Path) -> Result<SystemTime> {
194    Ok(std::fs::metadata(filepath)?.modified()?)
195}
196
197fn is_file_modified(path: &Path, original_modification: std::time::SystemTime) -> Result<bool> {
198    Ok(get_modified_date(path)? > original_modification)
199}
200
201fn get_new_filenames(temp_file: &Path) -> Result<Vec<String>> {
202    let file = std::fs::File::open(temp_file)?;
203    let reader = std::io::BufReader::new(file);
204
205    let new_names: Vec<String> = reader
206        .lines()
207        .map_while(Result::ok)
208        .map(|line| line.trim().to_owned())
209        .filter(|line| !line.is_empty())
210        .collect();
211    Ok(new_names)
212}
213
214/// A `BulkExecutor` and a sender of [`FmEvents`].
215/// It's used to execute creation / renaming of multiple files at once.
216/// Obviously it's inspired by ranger.
217///
218/// Bulk holds a `BulkExecutor` only when bulk mode is present, `None` otherwise.
219///
220/// Once `ask_filenames` is executed, a new tmp file is created. It's filled with every filename
221/// of flagged files in current directory.
222/// Modifications of this file are watched in a separate thread.
223/// Once the file is written, its content is parsed and a confirmation is asked : `format_confirmation`
224/// Renaming or creating is done in bulk with `execute`.
225#[derive(Default)]
226pub struct Bulk {
227    bulk: Option<BulkExecutor>,
228}
229
230impl Bulk {
231    /// Reset bulk content to None, droping all created or renomed filename from previous execution.
232    pub fn reset(&mut self) {
233        self.bulk = None;
234    }
235
236    /// Ask user for filename.
237    ///
238    /// Creates a temp file with every flagged filename in current dir.
239    /// Modification of this file are then watched in a thread.
240    /// The mode will change to BulkAction once something is written in the file.
241    ///
242    /// # Errors
243    ///
244    /// May fail if the user can't create a file, read or write in /tmp
245    /// May also fail if the watching thread fail.
246    pub fn ask_filenames(
247        &mut self,
248        flagged_in_current_dir: Vec<PathBuf>,
249        current_tab_path_str: &str,
250    ) -> Result<()> {
251        self.bulk =
252            Some(BulkExecutor::new(flagged_in_current_dir, current_tab_path_str).ask_filenames()?);
253        Ok(())
254    }
255
256    pub fn watch_in_thread(&mut self, fm_sender: Arc<Sender<FmEvents>>) -> Result<()> {
257        if let Some(bulk) = &self.bulk {
258            bulk.watch_modification_in_thread(fm_sender)?
259        }
260        Ok(())
261    }
262
263    pub fn get_new_names(&mut self) -> Result<()> {
264        if let Some(bulk) = &mut self.bulk {
265            bulk.get_new_names()?;
266        }
267        Ok(())
268    }
269
270    /// String representation of the filetree modifications.
271    pub fn format_confirmation(&self) -> Vec<String> {
272        if let Some(bulk) = &self.bulk {
273            let mut lines: Vec<String> = bulk
274                .original_filepath
275                .iter()
276                .zip(bulk.new_filenames.iter())
277                .map(|(original, new)| {
278                    format!("RENAME: {original} -> {new}", original = original.display())
279                })
280                .collect();
281            for new in bulk.new_filenames.iter().skip(bulk.original_filepath.len()) {
282                lines.push(format!("CREATE: {new}"));
283            }
284            lines
285        } else {
286            vec![]
287        }
288    }
289
290    /// Execute the action parsed from the file.
291    ///
292    /// # Errors
293    ///
294    /// May fail if bulk is still set to None. It should never happen.
295    /// May fail if the new file can't be created or the flagged file can't be renamed.
296    pub fn execute(&mut self) -> Result<(OptionVecPairPathBuf, OptionVecPathBuf)> {
297        let Some(bulk) = &mut self.bulk else {
298            return Err(anyhow!("bulk shouldn't be None"));
299        };
300        let ret = bulk.execute();
301        self.reset();
302        ret
303    }
304
305    /// Optional temporary file where filenames are edited by the user
306    /// None if `self.bulk` is `None`.
307    pub fn temp_file(&self) -> Option<PathBuf> {
308        self.bulk.as_ref().map(|bulk| bulk.temp_file.to_owned())
309    }
310
311    pub fn is_empty(&self) -> bool {
312        self.len() == 0
313    }
314
315    pub fn len(&self) -> usize {
316        let Some(bulk) = &self.bulk else {
317            return 0;
318        };
319        bulk.len()
320    }
321
322    pub fn index(&self) -> usize {
323        let Some(bulk) = &self.bulk else {
324            return 0;
325        };
326        bulk.index
327    }
328
329    pub fn next(&mut self) {
330        let Some(bulk) = &mut self.bulk else {
331            return;
332        };
333        bulk.next()
334    }
335
336    pub fn prev(&mut self) {
337        let Some(bulk) = &mut self.bulk else {
338            return;
339        };
340        bulk.prev()
341    }
342
343    /// Set the index to a new value if the value is below the length.
344    pub fn set_index(&mut self, index: usize) {
345        let Some(bulk) = &mut self.bulk else {
346            return;
347        };
348        bulk.set_index(index)
349    }
350
351    pub fn style(&self, index: usize, style: &ratatui::style::Style) -> ratatui::style::Style {
352        let Some(bulk) = &self.bulk else {
353            return ratatui::style::Style::default();
354        };
355        bulk.style(index, style)
356    }
357}