diff options
Diffstat (limited to 'src/journal.rs')
-rw-r--r-- | src/journal.rs | 258 |
1 files changed, 258 insertions, 0 deletions
diff --git a/src/journal.rs b/src/journal.rs new file mode 100644 index 0000000..a0d7cda --- /dev/null +++ b/src/journal.rs @@ -0,0 +1,258 @@ +use crate::error::JournalError; +use crate::git; +use crate::template::Templates; +use chrono::{DateTime, Local}; +use glob::glob; +use regex::Regex; +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<Self, JournalError> { + 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<Self, JournalError> { + 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<PathBuf, JournalError> { + 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, + topics: &[PathBuf], + 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()); + let mut full_topics = vec![]; + for topic in topics.iter() { + let pathname = topic_path(self.dirname(), topic); + if !pathname.exists() { + return Err(JournalError::NoSuchTopic(topic.to_path_buf())); + } + full_topics.push(topic.display().to_string()); + } + context.insert("topics", &full_topics); + + 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<PathBuf, JournalError> { + 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<PathBuf, JournalError> { + 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())) + } + } + + pub fn list_drafts(&self) -> Result<(), JournalError> { + let prefix = format!("{}/", self.drafts().display()); + let pattern = format!("{}*.md", prefix); + let entries = + glob(&pattern).map_err(|err| JournalError::PatternError(pattern.to_string(), err))?; + for entry in entries { + let entry = entry.map_err(|err| JournalError::GlobError(pattern.to_string(), err))?; + let title = get_title(&entry)?; + let entry = entry.file_stem().unwrap().to_string_lossy(); + let entry = entry.strip_prefix(&prefix).unwrap_or(&entry); + println!("{} {}", entry, title); + } + Ok(()) + } + + 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 remove_draft(&self, filename: &Path) -> Result<(), JournalError> { + std::fs::remove_file(filename) + .map_err(|err| JournalError::RemoveDraft(filename.to_path_buf(), err))?; + 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::now().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 dirname = self.dirname(); + if !dirname.exists() { + return Err(JournalError::NoJournalDirectory(dirname.to_path_buf())); + } + + let pathname = topic_path(self.dirname(), path); + let parent = pathname.parent().unwrap(); + if !parent.is_dir() { + std::fs::create_dir_all(parent) + .map_err(|err| JournalError::CreateDirectory(parent.to_path_buf(), err))?; + } + + 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 { + dirname.join(format!("{}.mdwn", topic.display())) +} + +fn current_timestamp() -> String { + let now: DateTime<Local> = Local::now(); + now.to_rfc2822() +} + +fn get_title(filename: &Path) -> Result<String, JournalError> { + let text = std::fs::read(filename) + .map_err(|err| JournalError::ReadDraft(filename.to_path_buf(), err))?; + let text = String::from_utf8_lossy(&text); + let pat = Regex::new(r#"^\[\[!meta title="(?P<title>.*)"\]\]"#).unwrap(); + if let Some(caps) = pat.captures(&text) { + if let Some(m) = caps.name("title") { + return Ok(m.as_str().to_string()); + } + } + Ok("(untitled)".to_string()) +} |