Week 05 of 2024

Development log of Tad Notes

8 items
  1. Write a meaningful message for missing certificate
  2. Separate and parameterize make development goals
  3. Every note will have a link to index
  4. Stub tags sub-command
  5. Write descriptions of all sub-commands
  6. Set default log level for development goals
  7. Print helpful message if notes directory is missing
  8. Implement listing all tags

Write a meaningful message for missing certificate

On by Tad Lispy

index cc6b445..991aaf7 100644
--- a/src/imap_import.rs
+++ b/src/imap_import.rs
@@ -122,7 +122,9 @@ fn connect(config: &Config) -> imap::Session<TlsStream<TcpStream>> {
=    let mut tls = native_tls::TlsConnector::builder();
=    if let Some(cert_path) = &config.cart_path {
=        log::debug!("Loading root certificate from {}", &cert_path);
-        let pem = &fs::read(cert_path).unwrap();
+        let pem = &fs::read(cert_path).expect(&format!(
+            "there should be a PEM certificate for the IMAP server at {cert_path}"
+        ));
=        let cert = native_tls::Certificate::from_pem(pem).unwrap();
=        tls.add_root_certificate(cert);
=    }

Separate and parameterize make development goals

On by Tad Lispy

For easier testing of sub-commands. The source and destination directories can be set via variables.

index 7fd0720..859c531 100644
--- a/Makefile
+++ b/Makefile
@@ -27,13 +27,35 @@ install: build
=
=develop: ## Rebuild and run a development version of the program
=develop: log-level ?= info
-develop:
-	RUST_LOG=$(log-level) cargo run -- import notes
-	RUST_LOG=$(log-level) cargo run -- list notes
-	RUST_LOG=$(log-level) cargo run -- export notes exported
-	miniserve exported
+develop: import list export
=.PHONY: develop
=
+
+import: ## Try importing the notes from an IMAP server
+import: notes ?= notes
+import:
+	RUST_LOG=$(log-level) cargo run -- import $(notes)
+.PHONY: import
+
+list: ## Try listing the notes
+list: notes ?= notes
+list:
+	RUST_LOG=$(log-level) cargo run -- list $(notes)
+.PHONY: list
+
+export: ## Try exporting the notes
+export: notes ?= notes
+export: exported ?= exported
+export:
+	RUST_LOG=$(log-level) cargo run -- export $(notes) $(exported)
+.PHONY: export
+
+serve: ## Serve exported notes
+serve: exported ?= exported
+serve:
+	miniserve --index=index.html --interfaces=127.0.0.1 $(exported)
+.PHONY: serve
+
=clean: ## Remove all build artifacts
=clean:
=	git clean -dfX \

On by Tad Lispy

index 04db96e..5d4e175 100644
--- a/templates/note.html
+++ b/templates/note.html
@@ -12,6 +12,11 @@
=
=    </head>
=    <body>
+        <nav>
+            <ul>
+                <li><a href="index.html">Index</a></li>
+            </ul>
+        </nav>
=        <header>
=            <h1>{{ title }}</h1>
=            <p>{{ date }}</p>

Stub tags sub-command

On by Tad Lispy

index 859c531..0d985b0 100644
--- a/Makefile
+++ b/Makefile
@@ -37,6 +37,12 @@ import:
=	RUST_LOG=$(log-level) cargo run -- import $(notes)
=.PHONY: import
=
+tags: ## Try listing the tags
+tags: notes ?= notes
+tags:
+	RUST_LOG=$(log-level) cargo run -- tags $(notes)
+.PHONY: tags
+
=list: ## Try listing the notes
=list: notes ?= notes
=list:
index e42f08d..7b14f43 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -23,6 +23,7 @@ struct Cli {
=enum Command {
=    Import { directory: PathBuf },
=    List { directory: PathBuf },
+    Tags { directory: PathBuf },
=    Export { from: PathBuf, into: PathBuf },
=}
=
@@ -34,6 +35,7 @@ fn main() {
=    match cli.command {
=        Command::Import { directory } => import(directory),
=        Command::List { directory } => list(directory),
+        Command::Tags { directory } => todo!("List tags"),
=        Command::Export { from, into } => export(from, into),
=    }
=}

Write descriptions of all sub-commands

On by Tad Lispy

index 7b14f43..228db2b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,9 +21,13 @@ struct Cli {
=
=#[derive(clap::Subcommand)]
=enum Command {
+    /// Import notes from an IMAP server
=    Import { directory: PathBuf },
+    /// List notes
=    List { directory: PathBuf },
+    /// List tags
=    Tags { directory: PathBuf },
+    /// Export notes to HTML
=    Export { from: PathBuf, into: PathBuf },
=}
=

Set default log level for development goals

On by Tad Lispy

Without it the logging was effectively disabled.

index 0d985b0..49b1d53 100644
--- a/Makefile
+++ b/Makefile
@@ -39,12 +39,14 @@ import:
=
=tags: ## Try listing the tags
=tags: notes ?= notes
+tags: log-level ?= info
=tags:
=	RUST_LOG=$(log-level) cargo run -- tags $(notes)
=.PHONY: tags
=
=list: ## Try listing the notes
=list: notes ?= notes
+list: log-level ?= info
=list:
=	RUST_LOG=$(log-level) cargo run -- list $(notes)
=.PHONY: list
@@ -52,12 +54,14 @@ list:
=export: ## Try exporting the notes
=export: notes ?= notes
=export: exported ?= exported
+export: log-level ?= info
=export:
=	RUST_LOG=$(log-level) cargo run -- export $(notes) $(exported)
=.PHONY: export
=
=serve: ## Serve exported notes
=serve: exported ?= exported
+serve: log-level ?= info
=serve:
=	miniserve --index=index.html --interfaces=127.0.0.1 $(exported)
=.PHONY: serve

On by Tad Lispy

Before that, given an incorrect path, the program would silently do noting.

index 228db2b..a9138f4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,6 +11,7 @@ use clap::Parser;
=use env_logger;
=use std::fs::create_dir_all;
=use std::path::PathBuf;
+use std::process::exit;
=
=#[derive(clap::Parser)]
=#[command(author, version, about, long_about = None)]
@@ -44,17 +45,21 @@ fn main() {
=    }
=}
=
-fn import(directory: PathBuf) {
+fn import(directory_path: PathBuf) {
+    check_directory(&directory_path);
+
=    let config = imap_import::Config::from_env();
=
=    // NOTE: Avoid leaking secrets in logs
=    #[cfg(debug_assertions)]
=    log::debug!("Configuration loaded {config:?}");
=
-    imap_import::import(&config, directory)
+    imap_import::import(&config, directory_path)
=}
=
=fn list(directory_path: PathBuf) {
+    check_directory(&directory_path);
+
=    log::info!("Listing notes");
=
=    for note in Notes::load(directory_path).list() {
@@ -68,3 +73,17 @@ fn export(from_directory: PathBuf, into_directory: PathBuf) {
=
=    notes.export_html(&into_directory)
=}
+
+fn check_directory(directory_path: &PathBuf) {
+    if !directory_path.exists() {
+        log::error!(
+            "Path doesn't exist: {path}",
+            path = directory_path.display()
+        );
+        exit(1)
+    }
+    if !directory_path.is_dir() {
+        log::error!("Not a directory: {path}", path = directory_path.display());
+        exit(2)
+    }
+}

Implement listing all tags

On by Tad Lispy

index a9138f4..1dfb454 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -39,8 +39,8 @@ fn main() {
=
=    match cli.command {
=        Command::Import { directory } => import(directory),
-        Command::List { directory } => list(directory),
-        Command::Tags { directory } => todo!("List tags"),
+        Command::List { directory } => list_notes(directory),
+        Command::Tags { directory } => list_tags(directory),
=        Command::Export { from, into } => export(from, into),
=    }
=}
@@ -57,7 +57,7 @@ fn import(directory_path: PathBuf) {
=    imap_import::import(&config, directory_path)
=}
=
-fn list(directory_path: PathBuf) {
+fn list_notes(directory_path: PathBuf) {
=    check_directory(&directory_path);
=
=    log::info!("Listing notes");
@@ -67,6 +67,16 @@ fn list(directory_path: PathBuf) {
=    }
=}
=
+fn list_tags(directory_path: PathBuf) {
+    check_directory(&directory_path);
+
+    log::info!("Listing tags");
+
+    for (tag, notes) in Notes::load(directory_path).tags() {
+        println!("{tag}\t{count}", count = notes.len())
+    }
+}
+
=fn export(from_directory: PathBuf, into_directory: PathBuf) {
=    create_dir_all(&into_directory).expect("ensuring the export directory exists shouldn't fail");
=    let notes = Notes::load(from_directory);
index cbbc7c9..24bfb5b 100644
--- a/src/note.rs
+++ b/src/note.rs
@@ -9,6 +9,7 @@ use pandoc;
=use pandoc::Pandoc;
=use serde::Deserialize;
=use slug::slugify;
+use std::collections::HashSet;
=use std::fs;
=use std::path::PathBuf;
=use yaml_front_matter::{Document, YamlFrontMatter};
@@ -61,6 +62,10 @@ impl Note {
=        self.document.metadata.title.clone()
=    }
=
+    pub fn tags(&self) -> HashSet<String> {
+        HashSet::from_iter(self.document.metadata.tags.clone())
+    }
+
=    pub fn date(&self) -> DateTime<Utc> {
=        self.document.metadata.date.clone()
=    }
index fc28d5d..3b15a35 100644
--- a/src/notes.rs
+++ b/src/notes.rs
@@ -22,6 +22,7 @@ impl Notes {
=
=        for note_path in note_paths {
=            let note_path = note_path.expect("The matching path should be correct");
+            log::debug!("Loading {path}", path = note_path.display());
=            let note = Note::read(note_path);
=            notes.insert(note);
=        }
@@ -71,6 +72,17 @@ impl Notes {
=        }
=        backlinks
=    }
+
+    pub fn tags(&self) -> HashMap<String, Vec<&Note>> {
+        let mut tags: HashMap<String, Vec<&Note>> = HashMap::default();
+        for note in self.list() {
+            println!("{title}", title = note.title());
+            for tag in note.tags() {
+                tags.entry(tag).or_default().push(note);
+            }
+        }
+        tags
+    }
=}
=
=impl From<Vec<Note>> for Notes {