bawa 0.1.0

A TUI game save organizer
Documentation
use crate::{event::Event, utils};
use anyhow::Result;
use notify::{
    event::{Event as NotifyEvent, ModifyKind, RenameMode},
    EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
};
use std::path::{Path, PathBuf};
use tokio::sync::mpsc::UnboundedSender;

pub struct Watcher(pub RecommendedWatcher);

impl Watcher {
    pub fn new(tx: UnboundedSender<Event>) -> Result<Self> {
        #[cfg(windows)]
        let watcher =
            notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
                use std::sync::Mutex;
                static RENAME_FROM: Mutex<Option<PathBuf>> = Mutex::new(None);

                let Ok(event) = res else { return };

                match event.kind {
                    EventKind::Create(_)
                    | EventKind::Modify(ModifyKind::Name(RenameMode::To))
                    | EventKind::Remove(_) => (),
                    EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
                        *RENAME_FROM.lock().unwrap() = Some(event.paths[0].clone());
                        return;
                    }
                    _ => return,
                }

                let file_name = event.paths[0].file_name().unwrap().to_string_lossy();
                if file_name.starts_with('.') || file_name == "active_game" {
                    *RENAME_FROM.lock().unwrap() = None;
                    return;
                }

                if let EventKind::Modify(ModifyKind::Name(RenameMode::To)) = event.kind {
                    tx.send(Event::FileSystem(FileSystemEvent::from_modify(
                        event,
                        RENAME_FROM.lock().unwrap().as_ref().unwrap(),
                    )))
                    .unwrap();
                    *RENAME_FROM.lock().unwrap() = None;
                } else {
                    tx.send(Event::FileSystem(FileSystemEvent::from(event)))
                        .unwrap();
                }
            })?;

        #[cfg(unix)]
        let watcher =
            notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
                let Ok(event) = res else { return };

                match event.kind {
                    EventKind::Create(_)
                    | EventKind::Modify(ModifyKind::Name(RenameMode::Both))
                    | EventKind::Remove(_) => (),
                    _ => return,
                }

                let file_name = event.paths[0].file_name().unwrap().to_string_lossy();
                if file_name.starts_with('.') || file_name == "active_game" {
                    return;
                }

                tx.send(Event::FileSystem(FileSystemEvent::from(event)))
                    .unwrap();
            })?;

        Ok(Self(watcher))
    }

    pub fn watch_recursive(&mut self, path: &Path) {
        self.0.watch(path, RecursiveMode::Recursive).unwrap();
    }

    pub fn watch_non_recursive(&mut self, path: &Path) {
        self.0.watch(path, RecursiveMode::NonRecursive).unwrap();
    }

    pub fn unwatch(&mut self, path: &Path) -> Result<()> {
        Ok(self.0.unwatch(path)?)
    }
}

pub struct FileSystemEvent {
    pub context: Context,
    pub kind: Kind,
    pub path: PathBuf,
}

#[derive(PartialEq, Eq)]
pub enum Context {
    Game,
    Profile,
    Entry,
}

pub enum Kind {
    Create,
    Rename(PathBuf),
    Delete,
}

impl FileSystemEvent {
    #[cfg(windows)]
    fn from_modify(event: NotifyEvent, from: &Path) -> Self {
        let kind = Kind::Rename(event.paths[0].clone());

        let count = utils::get_relative_path(&utils::get_state_dir().unwrap(), from)
            .unwrap()
            .iter()
            .count();

        let context = match count {
            1 => Context::Game,
            2 => Context::Profile,
            _ => Context::Entry,
        };

        Self {
            context,
            kind,
            path: from.to_owned(),
        }
    }
}

impl From<NotifyEvent> for FileSystemEvent {
    fn from(value: NotifyEvent) -> Self {
        let kind = match value.kind {
            EventKind::Create(_) => Kind::Create,
            EventKind::Modify(_) => Kind::Rename(value.paths[1].clone()),
            EventKind::Remove(_) => Kind::Delete,
            _ => unreachable!(),
        };

        let path = value.paths[0].clone();

        let count = utils::get_relative_path(&utils::get_state_dir().unwrap(), &path)
            .unwrap()
            .iter()
            .count();

        let context = match count {
            1 => Context::Game,
            2 => Context::Profile,
            _ => Context::Entry,
        };

        Self {
            context,
            kind,
            path,
        }
    }
}

pub trait HandleFileSystemEvent {
    fn on_create(&mut self, path: &Path) -> Result<()>;
    fn on_rename(&mut self, path: &Path, new_path: &Path) -> Result<()>;
    fn on_delete(&mut self, path: &Path) -> Result<()>;
    fn handle_file_system_event(&mut self, event: &FileSystemEvent) -> Result<()> {
        let path = &event.path;

        match event.kind {
            Kind::Create => self.on_create(path),
            Kind::Rename(ref new_path) => self.on_rename(path, new_path),
            Kind::Delete => self.on_delete(path),
        }
    }
}