use crate::error::JournalError; use chrono::Local; use std::path::{Path, PathBuf}; use std::process::Command; const MAX_DRAFT_COUNT: usize = 1000; pub struct Journal { dirname: PathBuf, entries: PathBuf, } 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))?; 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(), }) } 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(); Ok(Self { dirname, entries }) } else { Err(JournalError::NotAJournal(path.display().to_string())) } } fn dirname(&self) -> &Path { &self.dirname } fn drafts(&self) -> PathBuf { self.dirname().join("drafts") } fn entries(&self) -> PathBuf { self.entries.clone() } pub fn new_draft(&self, title: &str, 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 pathname = self.pick_file_id(&drafts)?; let text = format!(r#"[[!meta title="{}"]]"#, title); std::fs::write(&pathname, format!("{}\n\n", 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); } } return 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() { return Ok(filename); } else { return 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) })?; Ok(()) } } fn is_dir(path: &Path) -> bool { if let Ok(meta) = std::fs::symlink_metadata(path) { meta.is_dir() } else { false } }