Week 05 of 2024
Development log of Tad Notes
8 items
- Write a meaningful message for missing certificate
- Separate and parameterize make development goals
- Every note will have a link to index
- Stub tags sub-command
- Write descriptions of all sub-commands
- Set default log level for development goals
- Print helpful message if notes directory is missing
- Implement listing all tags
Write a meaningful message for missing certificate
On by
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
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 \Every note will have a link to index
On by
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
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
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
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: servePrint helpful message if notes directory is missing
On by
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
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 {