use crate::{create_temp_file, get_user_confirm, text_from_editor, Args, EncryptionScheme, Entry};
use ansi_term::{Colour::Red, Style};
use anyhow::{Context, Result};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp, error::Error, fs, io, io::prelude::*, str::FromStr};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Notebook {
pub file: String,
pub dt_format: String,
#[serde(skip)]
entries: Vec<Entry>,
sentiment: bool,
encryption: Option<EncryptionScheme>,
#[serde(skip)]
search_result: Vec<SearchResult>,
}
#[derive(Clone, Debug)]
struct SearchResult {
pub pattern: Regex,
pub entry_idx: usize,
pub location: Vec<String>,
}
impl SearchResult {
pub fn new(pattern: Regex, entry_idx: usize, location: Vec<String>) -> SearchResult {
SearchResult {
pattern,
entry_idx,
location,
}
}
}
impl Default for Notebook {
fn default() -> Self {
Self::new()
}
}
impl Notebook {
pub fn new() -> Notebook {
Notebook {
file: String::new(),
dt_format: String::new(),
entries: vec![],
sentiment: true,
encryption: None,
search_result: vec![],
}
}
pub fn write_entry(&self, entry: &Entry, path: &String) -> Result<&Self, Box<dyn Error>> {
let mut file = fs::OpenOptions::new()
.append(true)
.open(path)
.context(format!("unable to open or create '{}'", self.file))?;
file.write_all(format!("{entry}").as_bytes())
.context(format!("unable to write to '{}'", self.file))?;
Ok(self)
}
pub fn new_entry(&mut self, entry: Entry) -> Result<&Self, Box<dyn Error>> {
self.entries.push(entry);
Ok(self)
}
pub fn write_all_entries(&self) -> Result<&Self, Box<dyn Error>> {
let temp_file_path = create_temp_file(None);
assert!(!self.entries.is_empty()); for e in &self.entries {
self.write_entry(e, &temp_file_path)
.expect("Error writing to temp file");
}
fs::copy(&temp_file_path, &self.file)
.context(format!("unable to copy file to '{}'", &self.file))?;
fs::remove_file(&temp_file_path).expect("Couldn't remove temp file.");
Ok(self)
}
pub fn populate_notebook(mut self) -> Result<Self, Box<dyn Error>> {
let file =
fs::read_to_string(&self.file).context(format!("unable to open '{}'", self.file))?;
for e in file.split_terminator("¶\n") {
self.entries.push(
Entry::from_str(e).context(format!("could not read line '{}'", e.to_owned()))?,
);
}
Ok(self)
}
pub fn read_entry<W: Write>(&self, n: &usize, mut stdout: W) -> Result<&Self, Box<dyn Error>> {
let i = &self.entries.get(*n);
match i {
Some(e) => write!(stdout, "{e}").context("unable to display entry")?,
None => writeln!(stdout, "No such entry.")?,
}
Ok(self)
}
pub fn list_entries<W: Write>(
&self,
n: &usize,
mut stdout: W,
verbose: u8,
) -> Result<&Self, Box<dyn Error>> {
let i = cmp::min(self.entries.len(), *n);
for e in self.entries.iter().enumerate().skip(self.entries.len() - i) {
let substr = &e.1.text[..cmp::min(usize::from(verbose + 1) * 50, e.1.text.len())];
writeln!(
stdout,
"{}: {}\t{}…",
Style::new().bold().paint(e.0.to_string()),
Style::new().bold().paint(e.1.timestamp.to_string()),
substr,
)
.context("Error parsing something to string.")?;
}
Ok(self)
}
pub fn edit_entry(&mut self, n: usize) -> Result<&Self, Box<dyn Error>> {
let e = &mut self
.entries
.get_mut(n)
.expect("Unable to read entry, may not exist.");
let temp_file = create_temp_file(Some("notebook_entry"));
fs::write(&temp_file, &e.text).expect("Error writing to temp file");
let edited_entry = text_from_editor(Some(temp_file)).unwrap();
e.replace_text(&edited_entry);
Ok(self)
}
pub fn delete_entry(&mut self, n: usize, conf_req: bool) -> Result<&Self, Box<dyn Error>> {
if conf_req
&& get_user_confirm(
&mut io::stdin().lock(),
format!("Confirm delete entry {n}?"),
)
{
self.entries.remove(n);
println!("Deleted entry {n}");
} else {
self.entries.remove(n);
}
Ok(self)
}
pub fn run_command(mut self, cmd: Args) -> Result<Self, Box<dyn Error>> {
match cmd {
Args::New(e) => self.new_entry(e),
Args::List(ref n, l) => self.list_entries(n, &mut io::stdout(), l),
Args::Read(ref n) => self.read_entry(n, &mut io::stdout()),
Args::Edit(n) => self.edit_entry(n),
Args::Delete(n, conf) => self.delete_entry(n, conf),
Args::Search(s) => self
.search(s)
.unwrap()
.output_search_results(&mut io::stdout()),
Args::DateFilter(_s) => panic!("Not implemented"),
Args::DateSearch(_s) => panic!("Not implemented"),
Args::Unimplemented() => panic!("Not implemented"),
}
.expect("Error matching command");
Ok(self)
}
fn search(&mut self, q: String) -> Result<&Self, Box<dyn Error>> {
let regex = Regex::new(&q).expect("Error compiling regex.");
for (e_idx, e) in self.entries.iter().enumerate() {
if regex.is_match(&e.text) {
let matches = regex
.find_iter(&e.text)
.map(|digits| digits.as_str().to_owned())
.collect::<Vec<String>>();
self.search_result
.push(SearchResult::new(regex.clone(), e_idx, matches));
}
}
Ok(self)
}
fn _date_search(&mut self, q: String) -> Result<&Self, Box<dyn Error>> {
dbg!(&q);
Ok(self)
}
fn output_search_results<W: Write>(&self, mut stdout: W) -> Result<&Self, Box<dyn Error>> {
for r in &self.search_result {
write!(
stdout,
"{}: {}\t",
Style::new().bold().paint(r.entry_idx.to_string()),
Style::new()
.bold()
.paint(self.entries[r.entry_idx].timestamp.to_string())
)?;
for (idx, c) in r.pattern.split(&self.entries[r.entry_idx].text).enumerate() {
write!(stdout, "{c}")?;
if let Some(c) = r.location.get(idx) {
write!(stdout, "{}", Red.paint(c))?;
};
}
writeln!(stdout)?;
}
Ok(self)
}
}
#[cfg(test)]
mod test_notebook {
use super::*;
fn create_notebook() -> Notebook {
let mut nb = Notebook::new();
nb.file = "data/test.md".into();
nb.dt_format = "%A %e %B, %Y - %H:%M".into();
let nb = nb.populate_notebook().expect("Error reading notebook.");
nb
}
#[test]
fn test_populate_notebook() {
let mut nb = Notebook::new();
nb.file = "data/test.md".into();
nb.dt_format = "%A %e %B, %Y - %H:%M".into();
let nb = nb.populate_notebook().expect("Error reading notebook.");
assert_eq!(nb.entries.len(), 4);
}
#[test]
fn test_new_entry() {
let e = Entry::new("Testing this entry".into(), "%A %e %B, %Y - %H:%M");
let mut nb = create_notebook();
nb.new_entry(e).unwrap();
assert_eq!(nb.entries.len(), 5);
assert_eq!(nb.entries[4].text, "Testing this entry");
}
#[test]
fn test_list_one_entry() {
let mut stdout = vec![];
let nb = create_notebook();
nb.list_entries(&1, &mut stdout, 0).unwrap();
assert!(stdout.starts_with(
"\u{1b}[1m3\u{1b}[0m: \u{1b}[1m2021-05-13 22:17:00\u{1b}[0m\tA".as_bytes()
));
assert!(stdout.ends_with("happened: Lupin is disch…\n".as_bytes()));
}
#[test]
fn test_list_one_entry_verbose() {
let mut stdout = vec![];
let nb = create_notebook();
nb.list_entries(&1, &mut stdout, 1).unwrap();
assert!(stdout.starts_with(
"\u{1b}[1m3\u{1b}[0m: \u{1b}[1m2021-05-13 22:17:00\u{1b}[0m\tA".as_bytes()
));
assert!(stdout.ends_with("Mr. Perkupp’s office; and I scarcely …\n".as_bytes()));
}
#[test]
fn test_read_first_entry() {
let mut stdout = vec![];
let nb = create_notebook();
nb.read_entry(&0, &mut stdout).unwrap();
assert!(stdout.starts_with("### Friday 20 November, 2020 - 20:16".as_bytes()));
assert!(stdout.ends_with("Left out the Mutlars of course.\n\n¶\n".as_bytes()));
}
#[test]
fn test_read_outside_upper_bound() {
let mut stdout = vec![];
let nb = create_notebook();
nb.read_entry(&4, &mut stdout).unwrap();
assert_eq!(stdout, b"No such entry.\n");
}
#[test]
fn test_read_inside_upper_bound() {
let mut stdout = vec![];
let nb = create_notebook();
nb.read_entry(&3, &mut stdout).unwrap();
assert!(stdout.starts_with("### Thursday 13 May, 2021 - 22:17".as_bytes()));
assert!(stdout.ends_with("this seems an act of treachery.\n\n¶\n".as_bytes()));
}
#[test]
fn test_delete_entry() {
let mut stdout = vec![];
let mut nb = create_notebook();
assert_eq!(nb.entries.len(), 4);
nb.read_entry(&0, &mut stdout).unwrap();
nb.delete_entry(2, false).unwrap();
assert_eq!(nb.entries.len(), 3);
}
#[test]
fn test_search_word_single_result() {
let mut nb = create_notebook();
nb.search("brandy".into()).unwrap();
assert_eq!(nb.search_result.len(), 1);
}
#[test]
fn test_search_phrase_single_result() {
let mut nb = create_notebook();
nb.search("poisoned by some lobster".into()).unwrap();
assert_eq!(nb.search_result.len(), 1);
}
#[test]
fn test_search_multiple_results() {
let mut nb = create_notebook();
nb.search("Lupin".into()).unwrap();
assert_eq!(nb.search_result.len(), 3);
}
#[test]
fn test_search_zero_results() {
let mut nb = create_notebook();
nb.search("zebra".into()).unwrap();
assert_eq!(nb.search_result.len(), 0);
}
#[test]
fn test_search_daterange() {
let _nb: Notebook = create_notebook();
}
#[test]
fn test_search_correct_location() {
let mut nb = create_notebook();
nb.search("Crowbillon".into()).unwrap();
assert_eq!(nb.search_result[0].location[0], "Crowbillon");
nb.search("’".into()).unwrap();
assert_eq!(nb.search_result[1].location[1], "’");
}
#[test]
fn test_search_output() {
let mut stdout = vec![];
let mut nb = create_notebook();
nb.search("Crowbillon".into())
.unwrap()
.output_search_results(&mut stdout)
.unwrap();
assert!(&stdout.starts_with("\u{1b}[1m3\u{1b}[0m: \u{1b}[1m2021-05-13 22:17:00\u{1b}[0m\tA terrible misfortune has happened:".as_bytes()));
}
}