thought 2.0.0

Save your thoughts for later.
Documentation
use std::path::{Path, PathBuf};

use chrono::{DateTime, Utc};

use crate::{EditorWrapper, Error, ThoughtId, Truncate};

#[derive(Clone, Debug)]
pub struct Thought {
    thought_id: ThoughtId,
    date_time: DateTime<Utc>,
    file_path: PathBuf,
}

impl Thought {
    pub fn new<P: AsRef<Path>>(thoughts_dir: P) -> Result<Self, Error> {
        let thoughts_dir = thoughts_dir.as_ref().to_path_buf();
        if thoughts_dir.is_file() {
            return Err(Error::ThoughtsPathIsFile {
                invalid_thoughts_dir: thoughts_dir.to_string_lossy().to_string(),
            });
        }

        let thought_id = ThoughtId::new();
        let date_time = Utc::now();
        let file_path = thoughts_dir.join(format!("{}-{}.md", date_time.timestamp(), thought_id));

        Ok(Self {
            thought_id,
            date_time,
            file_path,
        })
    }

    pub fn from_file_path<P: AsRef<Path>>(file_path: P) -> Result<Self, Error> {
        let file_path = file_path.as_ref().to_path_buf();
        if file_path.is_dir() {
            return Err(Error::ThoughtPathIsDir {
                invalid_thought_path: file_path.to_string_lossy().to_string(),
            });
        }

        let file_stem = file_path
            .file_stem()
            .expect("file should always have a name")
            .to_string_lossy()
            .to_string();

        let mut split = file_stem.splitn(2, '-');
        let raw_timestamp = split.next().ok_or(Error::ParseThoughtFromPath {
            thought_path: file_path.to_string_lossy().to_string(),
        })?;
        let date_time = DateTime::from_timestamp(
            raw_timestamp.parse().map_err(|_| Error::ParseTimestamp {
                invalid_timestamp: raw_timestamp.to_string(),
            })?,
            0,
        )
        .ok_or(Error::ParseTimestamp {
            invalid_timestamp: raw_timestamp.to_string(),
        })?;
        let thought_id = split
            .next()
            .ok_or(Error::ParseThoughtFromPath {
                thought_path: file_path.to_string_lossy().to_string(),
            })?
            .parse()?;

        Ok(Self {
            thought_id,
            date_time,
            file_path,
        })
    }

    pub fn get_content(&self) -> Result<String, Error> {
        Ok(std::fs::read_to_string(&self.file_path).unwrap())
    }

    pub fn edit(&self, editor_wrapper: EditorWrapper) -> Result<(), Error> {
        editor_wrapper.edit(&self.file_path)
    }

    pub fn delete(&self) -> Result<(), Error> {
        std::fs::remove_file(&self.file_path).map_err(|_| Error::RemoveThought)?;
        Ok(())
    }

    pub fn id(&self) -> &ThoughtId {
        &self.thought_id
    }
}

impl std::fmt::Display for Thought {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let thought_content = self
            .get_content()
            .unwrap_or("".to_string())
            .trim()
            .lines()
            .next()
            .unwrap_or("")
            .to_string()
            .truncate_with_ellipsis(40);

        write!(
            f,
            "{} ({}) {}",
            self.thought_id,
            self.date_time.naive_local().format("%Y-%m-%d"),
            thought_content,
        )
    }
}