summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-04-02 08:53:21 +0300
committerLars Wirzenius <liw@liw.fi>2021-04-02 11:56:56 +0300
commit743d3c65cc69deb9fc4429190288dc8846bcd250 (patch)
tree7a643a7f530cef6b222d3d39a725d64970e4e65a
parent5aee9bd2ae4eb9c09e27c002f80d3edf071ebdcb (diff)
downloadjt2-743d3c65cc69deb9fc4429190288dc8846bcd250.tar.gz
feat! support multiple drafts
This changes the command line syntax: subcommands edit and finish now require the draft id. We can change this later so that if there is only one draft, the program picks that one automatically. Also, new entries are 0.md, 1.md, etc, which is not going to be acceptable for real use, but this works minimally.
-rw-r--r--Cargo.lock50
-rw-r--r--Cargo.toml2
-rw-r--r--jt.md53
-rw-r--r--src/bin/jt2.rs51
-rw-r--r--src/error.rs20
-rw-r--r--src/journal.rs108
-rw-r--r--src/lib.rs1
-rw-r--r--src/opt.rs13
-rw-r--r--subplot/jt.py29
-rw-r--r--subplot/jt.yaml6
10 files changed, 287 insertions, 46 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f7f4a1b..035e089 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -36,6 +36,12 @@ dependencies = [
]
[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -48,6 +54,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time",
+ "winapi",
+]
+
+[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -145,9 +164,11 @@ name = "jt2"
version = "0.1.0"
dependencies = [
"anyhow",
+ "chrono",
"directories-next",
"log",
"pretty_env_logger",
+ "regex",
"serde",
"serde_yaml",
"structopt",
@@ -188,6 +209,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
name = "pretty_env_logger"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -393,6 +433,16 @@ dependencies = [
]
[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "unicode-segmentation"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index a8e81bf..a76755a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,3 +13,5 @@ log = "0.4"
directories-next = "2.0.0"
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.8"
+regex = "1"
+chrono = "0.4"
diff --git a/jt.md b/jt.md
index cbea089..ad40b35 100644
--- a/jt.md
+++ b/jt.md
@@ -26,7 +26,7 @@ until it's finished.
$ jt --dirname ~/Journal init default "My private journal"
$ jt new --tag meta --tag journalling "My first journal entry"
... # text editor is opened so the new entry can be written
-$ jt finish
+$ jt finish 0 first-entry
~~~
@@ -157,11 +157,11 @@ and there is one draft in jrnl
and draft 0 in jrnl contains "Abracadabra"
given an executable script append.sh
-when I run jt2 --editor=./append.sh --dirname=jrnl edit
+when I run jt2 --editor=./append.sh --dirname=jrnl edit 0
then command is successful
and draft 0 in jrnl contains "Open sesame!"
-when I run jt2 --dirname=jrnl finish
+when I run jt2 --dirname=jrnl finish 0 abra
then command is successful
and there is one journal entry in jrnl, at FILE
and journal entry <FILE> contains "Abracadabra"
@@ -175,6 +175,53 @@ set -eux
echo "Open sesame!" >> "$1"
~~~
+## Create two drafts
+
+Verify that we can create two draft entries at the same time.
+
+~~~scenario
+given an installed jt
+
+when I run jt2 --dirname jrnl init default "My test journal"
+then command is successful
+then there are no drafts in jrnl
+then there are no journal entries in jrnl
+
+when I run jt2 --editor=none --dirname=jrnl new "Abracadabra"
+then command is successful
+then there is one draft in jrnl
+then draft 0 in jrnl contains "Abracadabra"
+
+when I run jt2 --editor=none --dirname=jrnl new "Simsalabim"
+then command is successful
+then there are two drafts in jrnl
+then draft 0 in jrnl contains "Abracadabra"
+then draft 1 in jrnl contains "Simsalabim"
+
+given an executable script append.sh
+when I run jt2 --editor=./append.sh --dirname=jrnl edit 0
+then draft 0 in jrnl contains "Open sesame!"
+when I run jt2 --editor=./append.sh --dirname=jrnl edit 1
+then draft 1 in jrnl contains "Open sesame!"
+
+when I run jt2 --dirname=jrnl finish 0 abra
+then command is successful
+then there is one journal entry in jrnl, at FILE
+then journal entry <FILE> contains "Abracadabra"
+then journal entry <FILE> contains "Open sesame!"
+then there is one draft in jrnl
+
+when I run jt2 --dirname=jrnl finish 1 sim
+then command is successful
+then there are two journal entries in jrnl, at FILE1 and FILE2
+then journal entry <FILE1> contains "Abracadabra"
+then journal entry <FILE2> contains "Simsalabim"
+then there are no drafts in jrnl
+~~~
+
+
+
+
# Colophon
diff --git a/src/bin/jt2.rs b/src/bin/jt2.rs
index cf55aef..3d85229 100644
--- a/src/bin/jt2.rs
+++ b/src/bin/jt2.rs
@@ -1,11 +1,9 @@
use jt2::config::Configuration;
use jt2::error::JournalError;
+use jt2::journal::Journal;
use jt2::opt::{Opt, SubCommand};
-use log::debug;
-use std::fs;
use std::path::Path;
-use std::process::Command;
use structopt::StructOpt;
fn main() -> anyhow::Result<()> {
@@ -20,57 +18,40 @@ fn main() -> anyhow::Result<()> {
} => init(&config.dirname, &journalname, &description)?,
SubCommand::IsJournal => is_journal(&config.dirname)?,
SubCommand::New { title } => new_draft(&title, &config.dirname, &config.editor)?,
- SubCommand::Edit => edit_draft(&config.dirname, &config.editor)?,
- SubCommand::Finish => finish_draft(&config.dirname)?,
+ SubCommand::Edit { draft } => edit_draft(&config.dirname, &config.editor, &draft)?,
+ SubCommand::Finish { draft, basename } => finish_draft(&config.dirname, &draft, &basename)?,
}
Ok(())
}
fn init(dirname: &Path, _journalname: &str, _description: &str) -> anyhow::Result<()> {
- std::fs::create_dir(dirname)
- .map_err(|err| JournalError::CreateDirectory(dirname.to_path_buf(), err))?;
+ Journal::init(dirname)?;
Ok(())
}
fn is_journal(dirname: &Path) -> anyhow::Result<()> {
- let meta = fs::symlink_metadata(dirname)?;
- if !meta.is_dir() {
+ if !Journal::is_journal(dirname) {
return Err(JournalError::NotAJournal(dirname.display().to_string()).into());
}
Ok(())
}
-fn new_draft(title: &str, dirname: &Path, _editor: &str) -> anyhow::Result<()> {
- let drafts = dirname.join("drafts");
- if !drafts.exists() {
- std::fs::create_dir(&drafts)?;
- }
- let draft_filename = drafts.join("0.md");
- std::fs::write(draft_filename, title)?;
+fn new_draft(title: &str, dirname: &Path, editor: &str) -> anyhow::Result<()> {
+ let journal = Journal::new(dirname)?;
+ journal.new_draft(title, editor)?;
Ok(())
}
-fn edit_draft(dirname: &Path, editor: &str) -> anyhow::Result<()> {
- debug!("edit_draft: dirname={:?}", dirname);
- debug!("edit_draft: editor={:?}", editor);
- let drafts = dirname.join("drafts");
- let draft_filename = drafts.join("0.md");
- debug!("edit_draft: draft_filename={:?}", draft_filename);
- Command::new(editor).arg(draft_filename).status()?;
- debug!("edit_draft: editor finished");
+fn edit_draft(dirname: &Path, editor: &str, draft: &str) -> anyhow::Result<()> {
+ let journal = Journal::new(dirname)?;
+ let filename = journal.pick_draft(draft)?;
+ journal.edit_draft(editor, &filename)?;
Ok(())
}
-fn finish_draft(dirname: &Path) -> anyhow::Result<()> {
- let drafts = dirname.join("drafts");
- let draft = drafts.join("0.md");
-
- let entries = dirname.join("entries");
- if !entries.exists() {
- std::fs::create_dir(&entries)?;
- }
- let entry = entries.join("0.md");
-
- std::fs::rename(draft, entry)?;
+fn finish_draft(dirname: &Path, draft: &str, basename: &str) -> anyhow::Result<()> {
+ let journal = Journal::new(dirname)?;
+ let filename = journal.pick_draft(draft)?;
+ journal.finish_draft(&filename, basename)?;
Ok(())
}
diff --git a/src/error.rs b/src/error.rs
index 00dae56..232eaec 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -25,4 +25,24 @@ pub enum JournalError {
/// Failed to create the directory for the journal.
#[error("failed to create journal directory {0}")]
CreateDirectory(PathBuf, #[source] std::io::Error),
+
+ /// To many drafts.
+ #[error("there are already {0} drafts in {1}, can't create more")]
+ TooManyDrafts(usize, PathBuf),
+
+ /// User chose a draft that doesn't exist.
+ #[error("No draft {0} in {1}")]
+ NoSuchDraft(String, PathBuf),
+
+ /// Failed to read draft.
+ #[error("failed to drafts {0}: {1}")]
+ ReadDraft(PathBuf, #[source] std::io::Error),
+
+ /// Draft is not UTF8.
+ #[error("draft {0} is not UTF8: {1}")]
+ DraftNotUUtf8(PathBuf, #[source] std::string::FromUtf8Error),
+
+ /// Failed to get metadata for specific file in drafts folder.
+ #[error("failed to stat draft in {0}: {1}")]
+ StatDraft(PathBuf, #[source] std::io::Error),
}
diff --git a/src/journal.rs b/src/journal.rs
new file mode 100644
index 0000000..911f9a3
--- /dev/null
+++ b/src/journal.rs
@@ -0,0 +1,108 @@
+use crate::error::JournalError;
+use chrono::Local;
+use log::debug;
+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))?;
+ 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()));
+ }
+ }
+
+ pub fn edit_draft(&self, editor: &str, filename: &Path) -> anyhow::Result<()> {
+ debug!("edit_draft: editor={:?}", editor);
+ debug!("edit_draft: filename={:?}", filename);
+ Command::new(editor).arg(filename).status()?;
+ debug!("edit_draft: editor finished");
+ 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(())
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 523317b..e988c12 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
pub mod config;
pub mod error;
+pub mod journal;
pub mod opt;
diff --git a/src/opt.rs b/src/opt.rs
index 12c3d26..df83659 100644
--- a/src/opt.rs
+++ b/src/opt.rs
@@ -63,8 +63,17 @@ pub enum SubCommand {
},
/// Invoke editor on journal entry draft.
- Edit,
+ Edit {
+ /// Draft id.
+ draft: String,
+ },
/// Finish a journal entry draft.
- Finish,
+ Finish {
+ /// Draft id.
+ draft: String,
+
+ /// Set base name of published file.
+ basename: String,
+ },
}
diff --git a/subplot/jt.py b/subplot/jt.py
index 31bceb7..312a22a 100644
--- a/subplot/jt.py
+++ b/subplot/jt.py
@@ -65,17 +65,22 @@ def output_contains(ctx, pattern=None):
def journal_has_no_drafts(ctx, dirname=None):
- assert_eq = globals()["assert_eq"]
- logging.debug(f"checking {dirname} has no drafts")
- drafts = os.path.join(dirname, "drafts")
- assert_eq(_find_files(drafts), [])
+ _journal_has_n_drafts(ctx, 0, dirname=dirname)
def journal_has_one_draft(ctx, dirname=None):
+ _journal_has_n_drafts(ctx, 1, dirname=dirname)
+
+
+def journal_has_two_drafts(ctx, dirname=None):
+ _journal_has_n_drafts(ctx, 2, dirname=dirname)
+
+
+def _journal_has_n_drafts(ctx, n, dirname=None):
assert_eq = globals()["assert_eq"]
- logging.debug(f"checking {dirname} has one draft")
+ logging.debug(f"checking {dirname} has {n} drafts")
drafts = os.path.join(dirname, "drafts")
- assert_eq(len(_find_files(drafts)), 1)
+ assert_eq(len(_find_files(drafts)), n)
def journal_has_no_entries(ctx, dirname=None):
@@ -98,6 +103,18 @@ def journal_has_one_entry(ctx, dirname=None, variable=None):
ctx["variables"] = variables
+def journal_has_two_entries(ctx, dirname=None, variable1=None, variable2=None):
+ assert_eq = globals()["assert_eq"]
+ logging.debug(f"checking {dirname} has two entries")
+ entries = os.path.join(dirname, "entries")
+ files = list(sorted(_find_files(entries)))
+ assert_eq(len(files), 2)
+ variables = ctx.get("variables", {})
+ variables[variable1] = files[0]
+ variables[variable2] = files[1]
+ ctx["variables"] = variables
+
+
def _find_files(root):
if not os.path.exists(root):
return []
diff --git a/subplot/jt.yaml b/subplot/jt.yaml
index 9c0983c..187f436 100644
--- a/subplot/jt.yaml
+++ b/subplot/jt.yaml
@@ -37,6 +37,9 @@
- then: there is one draft in {dirname}
function: journal_has_one_draft
+- then: there are two drafts in {dirname}
+ function: journal_has_two_drafts
+
- then: draft {draftno} in {dirname} contains "{pattern:text}"
function: draft_contains_string
@@ -46,5 +49,8 @@
- then: there is one journal entry in {dirname}, at {variable}
function: journal_has_one_entry
+- then: there are two journal entries in {dirname}, at {variable1} and {variable2}
+ function: journal_has_two_entries
+
- then: journal entry <{variable}> contains "{pattern:text}"
function: file_contains