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