From 33683712604607d85f20cbf93a7ac904552f4979 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 5 Sep 2020 14:42:08 +0300 Subject: feat: create and initialise a new journal At the moment this only creates a new directory, but most of the change is scaffolding so that more interesting changes can be built on top of this later. --- .gitignore | 5 ++ Cargo.lock | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 +++ check | 63 +++++++++++++++ jt.md | 33 +++++++- jt.py | 28 +++++++ jt.yaml | 16 ++++ runcmd.py | 77 +++++++++++++++++++ runcmd.yaml | 13 ++++ src/lib.rs | 1 + src/main.rs | 54 +++++++++++++ 11 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100755 check create mode 100644 jt.py create mode 100644 jt.yaml create mode 100644 runcmd.py create mode 100644 runcmd.yaml create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b10284 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +*.pdf +*.html +test.py +test.log diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8f97a58 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,251 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +dependencies = [ + "libc", +] + +[[package]] +name = "jt2" +version = "0.1.0" +dependencies = [ + "anyhow", + "structopt", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175c513d55719db99da20232b06cda8bab6b83ec2d04e3283edf0213c37c1a29" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc388d94ffabf39b5ed5fadddc40147cb21e605f53db6f8f36a625d27489ac5" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2513111825077552a6751dfad9e11ce0fba07d7276a3943a037d7e93e64c5f" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2066c46 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jt2" +version = "0.1.0" +authors = ["Lars Wirzenius "] +edition = "2018" + +[dependencies] +structopt = "0.3" +anyhow = "1" +thiserror = "1" diff --git a/check b/check new file mode 100755 index 0000000..753de3d --- /dev/null +++ b/check @@ -0,0 +1,63 @@ +#!/bin/sh + +set -eu + +verbose=false +if [ "$#" -gt 0 ] +then + case "$1" in + verbose | -v | --verbose) + verbose=true + ;; + esac +fi + +hideok= + +if command -v chronic > /dev/null +then + hideok=chronic +fi + +if $verbose +then + hideok= +fi + + +codegen() { + $hideok sp-codegen "$1" --output "$2" --run +} + +docgen() { + $hideok sp-docgen "$1" --output "$2" +} + +$hideok cargo build --all-targets +if cargo --list | awk '{ print $1 }' | grep 'clippy$' > /dev/null +then + $hideok cargo clippy +fi +$hideok cargo test + +if command -v rustfmt > /dev/null +then + find src -type f -name '*.rs' -exec rustfmt --check '{}' + +fi + +if command -v black > /dev/null +then + $hideok find . -type f -name '*.py' ! -name template.py ! -name test.py \ + -exec black --check '{}' + +fi + +for md in [^CR]*.md +do + $hideok echo "$md =====================================" + codegen "$md" test.py + docgen "$md" "$(basename "$md" .md).pdf" + docgen "$md" "$(basename "$md" .md).html" + $hideok echo +done + +echo "Everything seems to be in order." diff --git a/jt.md b/jt.md index c765d03..c8beace 100644 --- a/jt.md +++ b/jt.md @@ -23,17 +23,48 @@ journal for the user, and a new journal entry. The entry is a draft until it's finished. ~~~sh -$ jt create ~/Journal default "My private journal" +$ jt init ~/Journal 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 ~~~ +# Acceptance criteria and their verification + +This chapter defines detailed acceptance criteria and how they're +verified using *scenarios* for the [Subplot][] tool. + +[Subplot]: https://subplot.liw.fi/ + +## Create a new local journal repository + +`jt` works on a local repository, and it can be created an initialised +using the tool. + +~~~scenario +when I run jt init jrnl default "My test journal" +then program finished successfully +and directory jrnl exists + +when I run jt is-journal jrnl +then program finished successfully + +when I run jt is-journal bogus +then exit code is non-zero +~~~ + + --- title: "jt—a journalling tool" author: - Lars Wirzenius - Daniel Silverstone +bindings: +- jt.yaml +- runcmd.yaml +functions: +- jt.py +- runcmd.py ... diff --git a/jt.py b/jt.py new file mode 100644 index 0000000..2c5a4fc --- /dev/null +++ b/jt.py @@ -0,0 +1,28 @@ +import logging +import os + + +def run_jt_init(ctx, dirname=None, journalname=None, title=None): + runcmd(ctx, [_binary("jt"), "init", dirname, journalname, title]) + + +def run_jt_list_journals(ctx): + runcmd(ctx, [_binary("jt"), "list-journals"]) + + +def run_jt_is_journal(ctx, dirname=None): + runcmd(ctx, [_binary("jt"), "is-journal", dirname]) + + +def _binary(name): + return os.path.join(srcdir, "target", "debug", "jt2") + + +def is_directory(ctx, dirname=None): + logging.debug("checking if %r is a directory", dirname) + assert_eq(os.path.isdir(dirname), True) + + +def output_contains(ctx, pattern=None): + logging.debug("checking if %r contains", ctx["stdout"], pattern) + assert_eq(pattern in ctx["stdout"], True) diff --git a/jt.yaml b/jt.yaml new file mode 100644 index 0000000..863dbeb --- /dev/null +++ b/jt.yaml @@ -0,0 +1,16 @@ +- when: I run jt init (?P\S+) (?P\S+) "(?P.*)" + regex: true + function: run_jt_init + +- when: I run jt list-journals + function: run_jt_list_journals + +- when: I run jt is-journal {dirname} + function: run_jt_is_journal + +- then: directory {dirname} exists + function: is_directory + +- then: output contains "(?P<pattern>.*)" + regex: true + function: output_contains diff --git a/runcmd.py b/runcmd.py new file mode 100644 index 0000000..7193c15 --- /dev/null +++ b/runcmd.py @@ -0,0 +1,77 @@ +# Some step implementations for running commands and capturing the result. + +import subprocess + + +# Run a command, capture its stdout, stderr, and exit code in context. +def runcmd(ctx, argv, **kwargs): + p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) + stdout, stderr = p.communicate("") + ctx["argv"] = argv + ctx["stdout"] = stdout.decode("utf-8") + ctx["stderr"] = stderr.decode("utf-8") + ctx["exit"] = p.returncode + + +# Check that latest exit code captured by runcmd was a specific one. +def exit_code_is(ctx, wanted): + if ctx.get("exit") != wanted: + print("context:", ctx.as_dict()) + assert_eq(ctx.get("exit"), wanted) + + +# Check that latest exit code captured by runcmd was not a specific one. +def exit_code_is_not(ctx, unwanted): + if ctx.get("exit") == unwanted: + print("context:", ctx.as_dict()) + assert_ne(ctx.get("exit"), unwanted) + + +# Check that latest exit code captured by runcmd was zero. +def exit_code_zero(ctx): + exit_code_is(ctx, 0) + + +# Check that latest exit code captured by runcmd was not zero. +def exit_code_nonzero(ctx): + exit_code_is_not(ctx, 0) + + +# Check that stdout of latest runcmd contains a specific string. +def stdout_contains(ctx, pattern=None): + stdout = ctx.get("stdout", "") + if pattern not in stdout: + print("pattern:", repr(pattern)) + print("stdout:", repr(stdout)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern in stdout, True) + + +# Check that stdout of latest runcmd does not contain a specific string. +def stdout_does_not_contain(ctx, pattern=None): + stdout = ctx.get("stdout", "") + if pattern in stdout: + print("pattern:", repr(pattern)) + print("stdout:", repr(stdout)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern not in stdout, True) + + +# Check that stderr of latest runcmd does contains a specific string. +def stderr_contains(ctx, pattern=None): + stderr = ctx.get("stderr", "") + if pattern not in stderr: + print("pattern:", repr(pattern)) + print("stderr:", repr(stderr)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern in stderr, True) + + +# Check that stderr of latest runcmd does not contain a specific string. +def stderr_does_not_contain(ctx, pattern=None): + stderr = ctx.get("stderr", "") + if pattern not in stderr: + print("pattern:", repr(pattern)) + print("stderr:", repr(stderr)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern not in stderr, True) diff --git a/runcmd.yaml b/runcmd.yaml new file mode 100644 index 0000000..02e5ee1 --- /dev/null +++ b/runcmd.yaml @@ -0,0 +1,13 @@ +- then: exit code is non-zero + function: exit_code_nonzero + +- then: output matches /(?P<pattern>.+)/ + function: stdout_contains + regex: true + +- then: stderr matches /(?P<pattern>.+)/ + function: stderr_contains + regex: true + +- then: program finished successfully + function: exit_code_zero diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0a7efe1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use std::fs; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; +use thiserror::Error; + +#[derive(Debug, StructOpt)] +#[structopt(about = "maintain a journal")] +enum JT { + Init { + #[structopt(help = "Directory where journal should be stored")] + dirname: PathBuf, + #[structopt(help = "Short name for journal")] + journalname: String, + #[structopt(help = "Short description of journal, its title")] + description: String, + }, + IsJournal { + #[structopt(help = "Directory that may or may not be a journal")] + dirname: PathBuf, + }, +} + +#[derive(Debug, Error)] +enum JournalError { + #[error("directory {0} is not a journal")] + NotAJournal(String), +} + +fn main() -> Result<()> { + let opt = JT::from_args(); + match opt { + JT::Init { + dirname, + journalname, + description, + } => init(&dirname, &journalname, &description)?, + JT::IsJournal { dirname } => is_journal(&dirname)?, + } + Ok(()) +} + +fn init(dirname: &Path, _journalname: &str, _description: &str) -> anyhow::Result<()> { + std::fs::create_dir(dirname)?; + Ok(()) +} + +fn is_journal(dirname: &Path) -> anyhow::Result<()> { + let meta = fs::symlink_metadata(dirname)?; + if !meta.is_dir() { + return Err(JournalError::NotAJournal(dirname.to_string_lossy().to_string()).into()); + } + Ok(()) +} -- cgit v1.2.1 From d4e8b0409d6bc99522a6a995349e9779b9c9cd62 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius <liw@liw.fi> Date: Sat, 12 Sep 2020 16:15:46 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 0a7efe1..4e0a887 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,7 @@ fn init(dirname: &Path, _journalname: &str, _description: &str) -> anyhow::Resul fn is_journal(dirname: &Path) -> anyhow::Result<()> { let meta = fs::symlink_metadata(dirname)?; if !meta.is_dir() { - return Err(JournalError::NotAJournal(dirname.to_string_lossy().to_string()).into()); + return Err(JournalError::NotAJournal(dirname.display().to_string()).into()); } Ok(()) } -- cgit v1.2.1