tip-files 0.1.0

Tickets in project
Documentation
use std::cmp::Ordering;
use std::collections::HashMap;
use std::io::{BufRead, Read};
use std::{
    fs::{self, File},
    io::{BufReader, Write},
    path::PathBuf,
};

pub struct Tips<'a> {
    root: PathBuf,
    w: &'a mut dyn Write,
}

impl<'a> Tips<'a> {
    pub fn new(root: PathBuf, w: &'a mut dyn Write) -> Self {
        Self { root, w }
    }

    pub fn list(&mut self, filter_states: &[&str]) {
        let Ok(walker) = fs::read_dir(self.root.clone()) else {
            return;
        };
        let mut map = walk_to_map(walker, filter_states);
        let mut keys = map.keys().cloned().collect::<Vec<_>>();
        keys.sort_by(|a, b| smart_sort(a, b));
        for k in keys {
            let Some(t) = map.get_mut(&k) else {
                continue;
            };
            writeln!(self.w, "{}", t.header()).unwrap();
        }
    }

    pub fn details(&mut self, id: &str) {
        let path = match self.find_file_by_id(id) {
            Some(value) => value,
            None => return,
        };
        let mut tip: Tip = path.into();
        writeln!(self.w, "{}", tip.header()).unwrap();
        let d = tip.details();
        if !d.is_empty() {
            writeln!(self.w, "\n{d}").unwrap();
        }
    }

    pub fn create(&mut self, title: &str) {
        let Ok(walker) = fs::read_dir(self.root.clone()) else {
            return;
        };
        let map = walk_to_map(walker, &["open", "closed"]);
        let mut keys = map.keys().cloned().collect::<Vec<_>>();
        keys.sort_by(|a, b| smart_sort(a, b));
        let mut id = String::from("1.tip");
        if let Some(last) = keys.last() {
            let (prefix, num) = de_label(last);
            let num: usize = num.parse().unwrap();
            if prefix.is_empty() {
                id = format!("{}.tip", num + 1);
            } else {
                id = format!("{prefix}-{}.tip", num + 1);
            }
        }
        let mut tipfile = self.root.clone();
        tipfile.push("open");
        tipfile.push(id);
        let mut tip = Tip::create(tipfile, title);
        writeln!(self.w, "{}", tip.header()).unwrap();
    }

    pub fn delete(&mut self, id: &str) {
        let Some(tipfile) = self.find_file_by_id(id) else {
            return;
        };
        fs::remove_file(tipfile).unwrap();
    }

    pub fn open(&mut self, id: &str) {
        let mut path = self.root.clone();
        path.push("closed");
        path.push(format!("{id}.tip"));
        let mut newpath = self.root.clone();
        newpath.push("open");
        newpath.push(format!("{id}.tip"));
        if path.is_file() {
            fs::rename(path, newpath).unwrap();
        }
    }

    pub fn close(&mut self, id: &str) {
        let mut path = self.root.clone();
        path.push("open");
        path.push(format!("{id}.tip"));
        let mut newpath = self.root.clone();
        newpath.push("closed");
        newpath.push(format!("{id}.tip"));
        if path.is_file() {
            fs::rename(path, newpath).unwrap();
        }
    }

    fn find_file_by_id(&mut self, id: &str) -> Option<PathBuf> {
        let mut path = self.root.clone();
        path.push("open");
        path.push(format!("{id}.tip"));
        if !path.is_file() {
            path = self.root.clone();
            path.push("closed");
            path.push(format!("{id}.tip"));
        }
        if !path.is_file() {
            return None;
        }
        Some(path)
    }
}

struct Tip {
    print_buf: Vec<u8>,
    path: PathBuf,
}

impl Tip {
    fn header(&mut self) -> &str {
        let state = self
            .path
            .parent()
            .unwrap()
            .file_name()
            .unwrap()
            .to_str()
            .unwrap();
        let file = File::open(&self.path).unwrap();
        let lines = BufReader::new(file).lines();
        let title = lines.map_while(Result::ok).next().unwrap_or_default();
        let id = self.path.file_stem().unwrap().to_str().unwrap();
        write!(self.print_buf, "{id} ({state}): {title}").unwrap();
        str::from_utf8(&self.print_buf).unwrap()
    }

    fn details(&mut self) -> &str {
        let file = File::open(&self.path).unwrap();
        BufReader::new(file)
            .read_to_end(&mut self.print_buf)
            .unwrap();
        let start = self.print_buf.iter().position(|b| *b == 10).unwrap_or(0);
        str::from_utf8(&self.print_buf[start..]).unwrap().trim()
    }

    fn create(path: PathBuf, title: &str) -> Self {
        let mut f = File::create(&path).unwrap();
        writeln!(f, "{title}").unwrap();
        path.into()
    }
}

impl From<PathBuf> for Tip {
    fn from(value: PathBuf) -> Self {
        Self {
            print_buf: vec![],
            path: value,
        }
    }
}

fn walk_to_map(states: fs::ReadDir, filter_states: &[&str]) -> HashMap<String, Tip> {
    let mut map = HashMap::<String, Tip>::new();
    for state in states {
        let Ok(state) = state else {
            continue;
        };
        let Ok(tips) = fs::read_dir(state.path()) else {
            continue;
        };
        let state = state.path();
        let state = state.file_name().unwrap().to_str().unwrap();
        if filter_states.is_empty() || filter_states.contains(&state) {
            fill_tips(&mut map, tips);
        }
    }
    map
}

fn fill_tips(map: &mut HashMap<String, Tip>, tips: fs::ReadDir) {
    for tip in tips {
        let Ok(tip) = tip else {
            continue;
        };
        let tip_path = tip.path();
        let Some(ext) = tip_path.extension() else {
            continue;
        };
        if ext != "tip" {
            continue;
        }
        let t: Tip = tip.path().to_path_buf().into();
        let id = tip
            .path()
            .file_stem()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string();
        map.insert(id, t);
    }
}

fn smart_sort(a: &str, b: &str) -> Ordering {
    let (ap, asn) = de_label(a);
    let (bp, bsn) = de_label(b);
    if ap < bp {
        return Ordering::Less;
    } else if ap > bp {
        return Ordering::Greater;
    }
    if let Ok(an) = asn.parse::<usize>()
        && let Ok(bn) = bsn.parse::<usize>()
    {
        if an < bn {
            return Ordering::Less;
        } else if an > bn {
            return Ordering::Greater;
        } else {
            return Ordering::Equal;
        }
    }
    if asn < bsn {
        return Ordering::Less;
    } else if asn > bsn {
        return Ordering::Greater;
    }
    Ordering::Equal
}

fn de_label(id: &str) -> (&str, &str) {
    let Some(idx) = id.find('-') else {
        return ("", id);
    };
    (&id[..idx], &id[idx + 1..])
}