summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers+gitlab@digital-scurf.org>2021-04-02 09:03:41 +0000
committerDaniel Silverstone <dsilvers+gitlab@digital-scurf.org>2021-04-02 09:03:41 +0000
commit575315c31c9c9ee5c5b7a0b8dea25e860a873752 (patch)
tree7a643a7f530cef6b222d3d39a725d64970e4e65a
parent33a8e6107f3282988db7dabd2f5f5eb407820561 (diff)
parent743d3c65cc69deb9fc4429190288dc8846bcd250 (diff)
downloadjt2-575315c31c9c9ee5c5b7a0b8dea25e860a873752.tar.gz
Merge branch 'easter' into 'main'
Debian packager and multi-draft support Closes #6 and #5 See merge request larswirzenius/jt!8
-rw-r--r--Cargo.lock50
-rw-r--r--Cargo.toml2
-rwxr-xr-xcheck1
-rw-r--r--debian/cargo-checksum.json0
-rw-r--r--debian/changelog6
-rw-r--r--debian/compat2
-rw-r--r--debian/control24
-rw-r--r--debian/copyright23
-rwxr-xr-xdebian/rules14
-rw-r--r--debian/source/format1
-rw-r--r--jt.md62
-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
18 files changed, 367 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/check b/check
index 1f92df9..55b9158 100755
--- a/check
+++ b/check
@@ -8,6 +8,7 @@ then
case "$1" in
verbose | -v | --verbose)
verbose=true
+ shift
;;
esac
fi
diff --git a/debian/cargo-checksum.json b/debian/cargo-checksum.json
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/debian/cargo-checksum.json
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..ec6edc0
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,6 @@
+jt2 (0.1.0-1) UNRELEASED; urgency=medium
+
+ * Initial packaging. This is not intended to be uploaded to Debian, so
+ no closing of an ITP bug.
+
+ -- Lars Wirzenius <liw@liw.fi> Sat, 28 Sep 2019 16:45:49 +0300
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..021ea30
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1,2 @@
+10
+
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..85c7d40
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,24 @@
+Source: jt2
+Maintainer: Lars Wirzenius <liw@liw.fi>
+Section: misc
+Priority: optional
+Standards-Version: 4.2.0
+Build-Depends:
+ debhelper (>= 10~),
+ build-essential,
+ black,
+ dh-cargo,
+ moreutils,
+ python3,
+ subplot,
+ texlive-fonts-recommended,
+ texlive-latex-base,
+ texlive-latex-recommended
+Homepage: https://jt2.liw.fi
+
+Package: jt2
+Architecture: any
+Depends: ${misc:Depends}, ${shlibs:Depends}
+Built-Using: ${cargo:Built-Using}
+Description: personal journalling tool
+ jt adds entries to a personal journal.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..7ee7188
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,23 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: jt2
+Upstream-Contact: Lars Wirzenius <liw@liw.fi>
+Source: http://git.liw.fi/jt2
+
+Files: *
+Copyright: 2021, Lars Wirzenius, Daniel Silverstone
+License: GPL-3+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ .
+ On a Debian system, you can find a copy of GPL version 3 at
+ /usr/share/common-licenses/GPL-3 .
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..2aece9a
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,14 @@
+#!/usr/bin/make -f
+
+%:
+ dh $@ --buildsystem cargo
+
+override_dh_auto_build:
+ true
+
+override_dh_auto_install:
+ cargo install --path=. --root=debian/jt2
+ find debian/jt2 -name '.crates*' -delete
+
+override_dh_auto_test:
+ ./check
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/jt.md b/jt.md
index dcd3c51..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,62 @@ 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
+
+This document is meant to be processed with the [Subplot][] program to
+typeset into HTML or PDF or to generate a program that automatically
+verifies that all acceptance criteria are met.
+
+[Subplot]: https://subplot.liw.fi/
+
---
title: "jt&mdash;a journalling tool"
author:
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