summaryrefslogtreecommitdiff
path: root/src/journal.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/journal.rs')
-rw-r--r--src/journal.rs258
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", &current_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())
+}