summaryrefslogtreecommitdiff
path: root/src/journal.rs
blob: 2adb48d8f8da1f659f57a4d4b666c215f94b69fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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,
}

impl Journal {
    pub fn is_journal(path: &Path) -> bool {
        if let Ok(meta) = std::fs::symlink_metadata(path) {
            meta.is_dir()
        } else {
            false
        }
    }

    pub fn init(path: &Path) -> Result<Self, JournalError> {
        std::fs::create_dir(path)
            .map_err(|err| JournalError::CreateDirectory(path.to_path_buf(), err))?;
        Ok(Self {
            dirname: path.to_path_buf(),
        })
    }

    pub fn new(path: &Path) -> Result<Self, JournalError> {
        let dirname = path.to_path_buf();
        if dirname.exists() {
            Ok(Self { dirname })
        } else {
            Err(JournalError::NotAJournal(dirname.display().to_string()))
        }
    }

    fn dirname(&self) -> &Path {
        &self.dirname
    }

    fn drafts(&self) -> PathBuf {
        self.dirname().join("drafts")
    }

    fn entries(&self) -> PathBuf {
        self.dirname().join("entries")
    }

    pub fn new_draft(&self, title: &str, editor: &str) -> anyhow::Result<()> {
        let drafts = self.drafts();
        if !drafts.exists() {
            std::fs::create_dir(&drafts)?;
        }

        let pathname = self.pick_file_id(&drafts)?;
        let text = format!(r#"[[!meta title="{}"]]"#, title);
        std::fs::write(&pathname, format!("{}\n\n", text))?;
        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);
            }
        }
        return 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() {
            return Ok(filename);
        } else {
            return Err(JournalError::NoSuchDraft(id.to_string(), self.drafts()));
        }
    }

    fn edit(&self, editor: &str, filename: &Path) -> Result<(), JournalError> {
        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) -> anyhow::Result<()> {
        self.edit(editor, filename)?;
        Ok(())
    }

    pub fn finish_draft(&self, filename: &Path, basename: &str) -> anyhow::Result<()> {
        let entries = self.entries();
        if !entries.exists() {
            std::fs::create_dir(&entries)?;
        }

        let subdir = entries.join(Local::today().format("%Y/%m/%d").to_string());
        std::fs::create_dir_all(&subdir)?;

        let entry = subdir.join(basename);
        std::fs::rename(filename, entry)?;
        Ok(())
    }
}