use crate::error::JournalError; use crate::git; use crate::template::Templates; use chrono::{DateTime, Local}; use std::path::{Path, PathBuf}; use std::process::Command; use tera::Context; const MAX_DRAFT_COUNT: usize = 1000; pub struct Journal { dirname: PathBuf, entries: PathBuf, templates: Templates, } impl Journal { pub fn is_journal(path: &Path, entries: &Path) -> bool { is_dir(path) && is_dir(entries) } pub fn init(path: &Path, entries: &Path) -> Result { std::fs::create_dir(path) .map_err(|err| JournalError::CreateDirectory(path.to_path_buf(), err))?; git::init(path)?; std::fs::create_dir(entries) .map_err(|err| JournalError::CreateDirectory(entries.to_path_buf(), err))?; Ok(Self { dirname: path.to_path_buf(), entries: entries.to_path_buf(), templates: Templates::new(path)?, }) } pub fn new(path: &Path, entries: &Path) -> Result { if Self::is_journal(path, entries) { let dirname = path.to_path_buf(); let entries = entries.to_path_buf(); let templates = Templates::new(path)?; Ok(Self { dirname, entries, templates, }) } else { Err(JournalError::NotAJournal(path.display().to_string())) } } fn dirname(&self) -> &Path { &self.dirname } fn relative(&self, path: &Path) -> Result { let path = path.strip_prefix(self.dirname()).map_err(|err| { JournalError::RelativePath(path.to_path_buf(), self.dirname().to_path_buf(), err) })?; Ok(path.to_path_buf()) } fn drafts(&self) -> PathBuf { self.dirname().join("drafts") } fn entries(&self) -> PathBuf { self.entries.clone() } pub fn new_draft( &self, title: &str, topic: &Option, editor: &str, ) -> Result<(), JournalError> { let drafts = self.drafts(); if !drafts.exists() { std::fs::create_dir(&drafts) .map_err(|err| JournalError::CreateDirectory(drafts.to_path_buf(), err))?; } let mut context = Context::new(); context.insert("title", title); context.insert("date", ¤t_timestamp()); if let Some(ref topic) = topic { let pathname = topic_path(self.dirname(), topic); if !pathname.exists() { return Err(JournalError::NoSuchTopic(topic.to_path_buf())); } context.insert("topic", &topic.display().to_string()); } let pathname = self.pick_file_id(&drafts)?; let text = self.templates.new_draft(&context)?; std::fs::write(&pathname, text) .map_err(|err| JournalError::WriteEntry(pathname.to_path_buf(), err))?; self.edit(editor, &pathname)?; Ok(()) } fn pick_file_id(&self, dirname: &Path) -> Result { for i in 0..MAX_DRAFT_COUNT { let basename = format!("{}.md", i); let pathname = dirname.join(basename); if !pathname.exists() { return Ok(pathname); } } Err(JournalError::TooManyDrafts( MAX_DRAFT_COUNT, dirname.to_path_buf(), )) } pub fn pick_draft(&self, id: &str) -> Result { let drafts = self.drafts(); let filename = drafts.join(format!("{}.md", id)); if filename.exists() { Ok(filename) } else { Err(JournalError::NoSuchDraft(id.to_string(), self.drafts())) } } fn edit(&self, editor: &str, filename: &Path) -> Result<(), JournalError> { if editor == "none" { return Ok(()); } match Command::new(editor).arg(filename).output() { Err(err) => Err(JournalError::SpawnEditor(filename.to_path_buf(), err)), Ok(output) => { if output.status.success() { Ok(()) } else { let stderr = String::from_utf8_lossy(&output.stderr); Err(JournalError::EditorFailed( filename.to_path_buf(), stderr.into_owned(), )) } } } } pub fn edit_draft(&self, editor: &str, filename: &Path) -> Result<(), JournalError> { self.edit(editor, filename)?; Ok(()) } pub fn finish_draft(&self, filename: &Path, basename: &str) -> Result<(), JournalError> { let entries = self.entries(); if !entries.exists() { std::fs::create_dir(&entries) .map_err(|err| JournalError::CreateDirectory(entries.to_path_buf(), err))?; } let subdir = entries.join(Local::today().format("%Y/%m/%d").to_string()); std::fs::create_dir_all(&subdir) .map_err(|err| JournalError::CreateDirectory(entries.to_path_buf(), err))?; let basename = PathBuf::from(format!("{}.mdwn", basename)); let entry = subdir.join(basename); std::fs::rename(filename, &entry).map_err(|err| { JournalError::RenameEntry(filename.to_path_buf(), entry.to_path_buf(), err) })?; let entry = self.relative(&entry)?; git::add(self.dirname(), &[&entry])?; let msg = format!("journal entry {}", entry.display()); git::commit(self.dirname(), &msg)?; Ok(()) } pub fn new_topic(&self, path: &Path, title: &str, editor: &str) -> Result<(), JournalError> { let mut context = Context::new(); context.insert("title", title); let pathname = topic_path(self.dirname(), path); let text = self.templates.new_topic(&context)?; std::fs::write(&pathname, text) .map_err(|err| JournalError::WriteTopic(pathname.to_path_buf(), err))?; self.edit(editor, &pathname)?; let topic = self.relative(&pathname)?; git::add(self.dirname(), &[&topic])?; let msg = format!("new topic {}", topic.display()); git::commit(self.dirname(), &msg)?; Ok(()) } } fn is_dir(path: &Path) -> bool { if let Ok(meta) = std::fs::symlink_metadata(path) { meta.is_dir() } else { false } } fn topic_path(dirname: &Path, topic: &Path) -> PathBuf { let mut path = dirname.join(topic); path.set_extension("mdwn"); path } fn current_timestamp() -> String { let now: DateTime = Local::now(); now.to_rfc2822() }