diff options
author | Daniel Silverstone <dsilvers+gitlab@digital-scurf.org> | 2021-04-02 09:03:41 +0000 |
---|---|---|
committer | Daniel Silverstone <dsilvers+gitlab@digital-scurf.org> | 2021-04-02 09:03:41 +0000 |
commit | 575315c31c9c9ee5c5b7a0b8dea25e860a873752 (patch) | |
tree | 7a643a7f530cef6b222d3d39a725d64970e4e65a | |
parent | 33a8e6107f3282988db7dabd2f5f5eb407820561 (diff) | |
parent | 743d3c65cc69deb9fc4429190288dc8846bcd250 (diff) | |
download | jt2-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.lock | 50 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rwxr-xr-x | check | 1 | ||||
-rw-r--r-- | debian/cargo-checksum.json | 0 | ||||
-rw-r--r-- | debian/changelog | 6 | ||||
-rw-r--r-- | debian/compat | 2 | ||||
-rw-r--r-- | debian/control | 24 | ||||
-rw-r--r-- | debian/copyright | 23 | ||||
-rwxr-xr-x | debian/rules | 14 | ||||
-rw-r--r-- | debian/source/format | 1 | ||||
-rw-r--r-- | jt.md | 62 | ||||
-rw-r--r-- | src/bin/jt2.rs | 51 | ||||
-rw-r--r-- | src/error.rs | 20 | ||||
-rw-r--r-- | src/journal.rs | 108 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/opt.rs | 13 | ||||
-rw-r--r-- | subplot/jt.py | 29 | ||||
-rw-r--r-- | subplot/jt.yaml | 6 |
18 files changed, 367 insertions, 46 deletions
@@ -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" @@ -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" @@ -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) @@ -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—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(()) + } +} @@ -1,3 +1,4 @@ pub mod config; pub mod error; +pub mod journal; pub mod opt; @@ -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 |