From e4e40eb0cae0bd4a01e0cacf896095db6ac0d78d Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 2 Apr 2021 08:08:08 +0300 Subject: fix: if ./check gets -v, don't pass it onto test.py --- check | 1 + 1 file changed, 1 insertion(+) 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 -- cgit v1.2.1 From 2608402e2bf194c5fa088dbbc8eac9d78cbf27fc Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 2 Apr 2021 07:40:39 +0300 Subject: build: add Debian packaging --- debian/cargo-checksum.json | 0 debian/changelog | 6 ++++++ debian/compat | 2 ++ debian/control | 24 ++++++++++++++++++++++++ debian/copyright | 23 +++++++++++++++++++++++ debian/rules | 14 ++++++++++++++ debian/source/format | 1 + 7 files changed, 70 insertions(+) create mode 100644 debian/cargo-checksum.json create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format diff --git a/debian/cargo-checksum.json b/debian/cargo-checksum.json new file mode 100644 index 0000000..e69de29 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 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 +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 +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 . + . + 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) -- cgit v1.2.1 From 5aee9bd2ae4eb9c09e27c002f80d3edf071ebdcb Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 2 Apr 2021 08:43:58 +0300 Subject: docs: add colophon to subplot --- jt.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/jt.md b/jt.md index dcd3c51..cbea089 100644 --- a/jt.md +++ b/jt.md @@ -175,6 +175,15 @@ set -eux echo "Open sesame!" >> "$1" ~~~ + +# 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: -- cgit v1.2.1 From 743d3c65cc69deb9fc4429190288dc8846bcd250 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 2 Apr 2021 08:53:21 +0300 Subject: 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. --- Cargo.lock | 50 ++++++++++++++++++++++++++ Cargo.toml | 2 ++ jt.md | 53 +++++++++++++++++++++++++-- src/bin/jt2.rs | 51 +++++++++----------------- src/error.rs | 20 +++++++++++ src/journal.rs | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/opt.rs | 13 +++++-- subplot/jt.py | 29 +++++++++++---- subplot/jt.yaml | 6 ++++ 10 files changed, 287 insertions(+), 46 deletions(-) create mode 100644 src/journal.rs diff --git a/Cargo.lock b/Cargo.lock index f7f4a1b..035e089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,12 @@ dependencies = [ "winapi", ] +[[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" @@ -47,6 +53,19 @@ version = "1.0.0" 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" @@ -145,9 +164,11 @@ name = "jt2" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "directories-next", "log", "pretty_env_logger", + "regex", "serde", "serde_yaml", "structopt", @@ -187,6 +208,25 @@ version = "2.3.4" 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" @@ -392,6 +432,16 @@ dependencies = [ "syn", ] +[[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" 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 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 contains "Abracadabra" +then journal entry 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 contains "Abracadabra" +then journal entry 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 { + 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 { + 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 { + 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 { + 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 -- cgit v1.2.1