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.trim()).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..])
}