fm-tui 0.2.3

FM : a file manager inspired by ranger and dired{n}{n}Config files ~/.config/fm/{n}Documentation https://github.com/qkzk/fm{n}
Documentation
use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
use std::sync::{mpsc::Sender, Arc};
use std::thread;
use std::time::{Duration, SystemTime};

use anyhow::{anyhow, Result};

use crate::common::{random_name, rename_filename, TMP_FOLDER_PATH};
use crate::event::FmEvents;
use crate::{log_info, log_line};

type OptionVecPathBuf = Option<Vec<PathBuf>>;
type OptionVecPairPathBuf = Option<Vec<(PathBuf, PathBuf)>>;

struct BulkExecutor {
    index: usize,
    original_filepath: Vec<PathBuf>,
    temp_file: PathBuf,
    new_filenames: Vec<String>,
    parent_dir: String,
}

impl BulkExecutor {
    fn new(original_filepath: Vec<PathBuf>, parent_dir: &str) -> Self {
        let temp_file = generate_random_filepath();
        Self {
            index: 0,
            original_filepath,
            temp_file,
            new_filenames: vec![],
            parent_dir: parent_dir.to_owned(),
        }
    }

    fn ask_filenames(self) -> Result<Self> {
        create_random_file(&self.temp_file)?;
        log_info!("created {temp_file}", temp_file = self.temp_file.display());
        self.write_original_names()?;
        // open_temp_file_with_editor(&self.temp_file, status)?;

        // self.watch_modification_in_thread(original_modification, fm_sender);
        Ok(self)
    }

    fn watch_modification_in_thread(&self, fm_sender: Arc<Sender<FmEvents>>) -> Result<()> {
        let original_modification = get_modified_date(&self.temp_file)?;
        let filepath = self.temp_file.to_owned();
        thread::spawn(move || {
            loop {
                if is_file_modified(&filepath, original_modification).unwrap_or(true) {
                    break;
                }
                thread::sleep(Duration::from_millis(100));
            }
            fm_sender.send(FmEvents::BulkExecute).unwrap_or_default();
        });
        Ok(())
    }
    fn get_new_names(&mut self) -> Result<()> {
        self.new_filenames = get_new_filenames(&self.temp_file)?;
        Ok(())
    }

    fn write_original_names(&self) -> Result<()> {
        let mut file = std::fs::File::create(&self.temp_file)?;
        log_info!("created {temp_file}", temp_file = self.temp_file.display());

        for path in &self.original_filepath {
            let Some(os_filename) = path.file_name() else {
                return Ok(());
            };
            let Some(filename) = os_filename.to_str() else {
                return Ok(());
            };
            file.write_all(filename.as_bytes())?;
            file.write_all(b"\n")?;
        }
        Ok(())
    }

    fn execute(&self) -> Result<(OptionVecPairPathBuf, OptionVecPathBuf)> {
        let paths = self.rename_create();
        self.del_temporary_file()?;
        paths
    }

    fn rename_create(&self) -> Result<(OptionVecPairPathBuf, OptionVecPathBuf)> {
        let updated_paths = self.rename_all(&self.new_filenames)?;
        let created_paths = self.create_all_files(&self.new_filenames)?;
        Ok((updated_paths, created_paths))
    }

    fn rename_all(&self, new_filenames: &[String]) -> Result<OptionVecPairPathBuf> {
        let mut updated_paths = vec![];
        for (old_path, filename) in self.original_filepath.iter().zip(new_filenames.iter()) {
            match rename_filename(old_path, filename) {
                Ok(new_path) => updated_paths.push((old_path.to_owned(), new_path)),
                Err(error) => log_info!(
                    "Error renaming {old_path} to {filename}. Error: {error:?}",
                    old_path = old_path.display()
                ),
            }
        }
        log_line!("Bulk renamed {len} files", len = updated_paths.len());
        Ok(Some(updated_paths))
    }

    fn create_all_files(&self, new_filenames: &[String]) -> Result<OptionVecPathBuf> {
        let mut paths = vec![];
        for filename in new_filenames.iter().skip(self.original_filepath.len()) {
            let Some(path) = self.create_file(filename)? else {
                continue;
            };
            paths.push(path)
        }
        log_line!("Bulk created {len} files", len = paths.len());
        Ok(Some(paths))
    }

    fn create_file(&self, filename: &str) -> Result<Option<PathBuf>> {
        let mut new_path = std::path::PathBuf::from(&self.parent_dir);
        if !filename.ends_with('/') {
            new_path.push(filename);
            let Some(parent) = new_path.parent() else {
                return Ok(None);
            };
            log_info!("Bulk new files. Creating parent: {}", parent.display());
            if std::fs::create_dir_all(parent).is_err() {
                return Ok(None);
            };
            log_info!("creating: {new_path:?}");
            std::fs::File::create(&new_path)?;
            log_line!("Bulk created {new_path}", new_path = new_path.display());
        } else {
            new_path.push(filename);
            log_info!("Bulk creating dir: {}", new_path.display());
            std::fs::create_dir_all(&new_path)?;
            log_line!("Bulk created {new_path}", new_path = new_path.display());
        }
        Ok(Some(new_path))
    }

    fn del_temporary_file(&self) -> Result<()> {
        std::fs::remove_file(&self.temp_file)?;
        Ok(())
    }

    fn len(&self) -> usize {
        self.new_filenames.len()
    }

    fn next(&mut self) {
        self.index = (self.index + 1) % self.len()
    }

    fn prev(&mut self) {
        if self.index > 0 {
            self.index -= 1
        } else {
            self.index = self.len() - 1
        }
    }

    /// Set the index to a new value if the value is below the length.
    fn set_index(&mut self, index: usize) {
        if index < self.len() {
            self.index = index;
        }
    }

    /// Reverse the received effect if the index match the selected index.
    fn style(&self, index: usize, style: &ratatui::style::Style) -> ratatui::style::Style {
        let mut style = *style;
        if index == self.index {
            style.add_modifier |= ratatui::style::Modifier::REVERSED;
        }
        style
    }
}

fn generate_random_filepath() -> PathBuf {
    let mut filepath = PathBuf::from(&TMP_FOLDER_PATH);
    filepath.push(random_name());
    filepath
}

fn create_random_file(temp_file: &Path) -> Result<()> {
    std::fs::File::create(temp_file)?;
    Ok(())
}

fn get_modified_date(filepath: &Path) -> Result<SystemTime> {
    Ok(std::fs::metadata(filepath)?.modified()?)
}

fn is_file_modified(path: &Path, original_modification: std::time::SystemTime) -> Result<bool> {
    Ok(get_modified_date(path)? > original_modification)
}

fn get_new_filenames(temp_file: &Path) -> Result<Vec<String>> {
    let file = std::fs::File::open(temp_file)?;
    let reader = std::io::BufReader::new(file);

    let new_names: Vec<String> = reader
        .lines()
        .map_while(Result::ok)
        .map(|line| line.trim().to_owned())
        .filter(|line| !line.is_empty())
        .collect();
    Ok(new_names)
}

/// A `BulkExecutor` and a sender of [`FmEvents`].
/// It's used to execute creation / renaming of multiple files at once.
/// Obviously it's inspired by ranger.
///
/// Bulk holds a `BulkExecutor` only when bulk mode is present, `None` otherwise.
///
/// Once `ask_filenames` is executed, a new tmp file is created. It's filled with every filename
/// of flagged files in current directory.
/// Modifications of this file are watched in a separate thread.
/// Once the file is written, its content is parsed and a confirmation is asked : `format_confirmation`
/// Renaming or creating is done in bulk with `execute`.
#[derive(Default)]
pub struct Bulk {
    bulk: Option<BulkExecutor>,
}

impl Bulk {
    /// Reset bulk content to None, droping all created or renomed filename from previous execution.
    pub fn reset(&mut self) {
        self.bulk = None;
    }

    /// Ask user for filename.
    ///
    /// Creates a temp file with every flagged filename in current dir.
    /// Modification of this file are then watched in a thread.
    /// The mode will change to BulkAction once something is written in the file.
    ///
    /// # Errors
    ///
    /// May fail if the user can't create a file, read or write in /tmp
    /// May also fail if the watching thread fail.
    pub fn ask_filenames(
        &mut self,
        flagged_in_current_dir: Vec<PathBuf>,
        current_tab_path_str: &str,
    ) -> Result<()> {
        self.bulk =
            Some(BulkExecutor::new(flagged_in_current_dir, current_tab_path_str).ask_filenames()?);
        Ok(())
    }

    pub fn watch_in_thread(&mut self, fm_sender: Arc<Sender<FmEvents>>) -> Result<()> {
        if let Some(bulk) = &self.bulk {
            bulk.watch_modification_in_thread(fm_sender)?
        }
        Ok(())
    }

    pub fn get_new_names(&mut self) -> Result<()> {
        if let Some(bulk) = &mut self.bulk {
            bulk.get_new_names()?;
        }
        Ok(())
    }

    /// String representation of the filetree modifications.
    pub fn format_confirmation(&self) -> Vec<String> {
        if let Some(bulk) = &self.bulk {
            let mut lines: Vec<String> = bulk
                .original_filepath
                .iter()
                .zip(bulk.new_filenames.iter())
                .map(|(original, new)| {
                    format!("RENAME: {original} -> {new}", original = original.display())
                })
                .collect();
            for new in bulk.new_filenames.iter().skip(bulk.original_filepath.len()) {
                lines.push(format!("CREATE: {new}"));
            }
            lines
        } else {
            vec![]
        }
    }

    /// Execute the action parsed from the file.
    ///
    /// # Errors
    ///
    /// May fail if bulk is still set to None. It should never happen.
    /// May fail if the new file can't be created or the flagged file can't be renamed.
    pub fn execute(&mut self) -> Result<(OptionVecPairPathBuf, OptionVecPathBuf)> {
        let Some(bulk) = &mut self.bulk else {
            return Err(anyhow!("bulk shouldn't be None"));
        };
        let ret = bulk.execute();
        self.reset();
        ret
    }

    /// Optional temporary file where filenames are edited by the user
    /// None if `self.bulk` is `None`.
    pub fn temp_file(&self) -> Option<PathBuf> {
        self.bulk.as_ref().map(|bulk| bulk.temp_file.to_owned())
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    pub fn len(&self) -> usize {
        let Some(bulk) = &self.bulk else {
            return 0;
        };
        bulk.len()
    }

    pub fn index(&self) -> usize {
        let Some(bulk) = &self.bulk else {
            return 0;
        };
        bulk.index
    }

    pub fn next(&mut self) {
        let Some(bulk) = &mut self.bulk else {
            return;
        };
        bulk.next()
    }

    pub fn prev(&mut self) {
        let Some(bulk) = &mut self.bulk else {
            return;
        };
        bulk.prev()
    }

    /// Set the index to a new value if the value is below the length.
    pub fn set_index(&mut self, index: usize) {
        let Some(bulk) = &mut self.bulk else {
            return;
        };
        bulk.set_index(index)
    }

    pub fn style(&self, index: usize, style: &ratatui::style::Style) -> ratatui::style::Style {
        let Some(bulk) = &self.bulk else {
            return ratatui::style::Style::default();
        };
        bulk.style(index, style)
    }
}