generated from Astrial.org/template
	feat: add basic Inventory feature
This commit is contained in:
		
							parent
							
								
									624e3457e1
								
							
						
					
					
						commit
						072466d285
					
				
					 5 changed files with 147 additions and 1 deletions
				
			
		
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							|  | @ -198,6 +198,7 @@ dependencies = [ | |||
|  "clap", | ||||
|  "iana-time-zone", | ||||
|  "indoc", | ||||
|  "regex", | ||||
|  "serde", | ||||
|  "toml", | ||||
| ] | ||||
|  |  | |||
|  | @ -9,5 +9,6 @@ chrono-tz = "0.10.0" | |||
| clap = { version = "4.5.23", features = ["derive"] } | ||||
| iana-time-zone = "0.1.61" | ||||
| indoc = "2.0.5" | ||||
| regex = "1.11.1" | ||||
| serde = { version = "1.0.216", features = ["derive"] } | ||||
| toml = "0.8.19" | ||||
|  |  | |||
|  | @ -4,7 +4,8 @@ use clap::{Parser, Subcommand}; | |||
| use diaryrs::macros::UnwrapOrFatalAble; | ||||
| use serde::Deserialize; | ||||
| 
 | ||||
| /// Overengineered and overly opinionated Rust-based diary keeper and processor
 | ||||
| /// Overengineered and overly opinionated Rust-based diary keeper and processor.
 | ||||
| /// Happy journaling!
 | ||||
| #[derive(Parser)] | ||||
| #[command(version, about)] | ||||
| pub struct Args { | ||||
|  | @ -18,6 +19,8 @@ pub enum Commands { | |||
|     Info, | ||||
|     /// Write diary entry file for today
 | ||||
|     Today, | ||||
|     /// Take inventory of the diary entries you have
 | ||||
|     Inventory, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Deserialize)] | ||||
|  |  | |||
							
								
								
									
										26
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								src/main.rs
									
									
									
									
									
								
							|  | @ -1,10 +1,12 @@ | |||
| mod args; | ||||
| mod mgmt; | ||||
| 
 | ||||
| use args::{Args, Config}; | ||||
| use chrono::Datelike; | ||||
| use clap::Parser; | ||||
| use diaryrs::{macros::UnwrapOrFatalAble, time::Time, util}; | ||||
| use indoc::formatdoc; | ||||
| use mgmt::Inventory; | ||||
| use std::fs; | ||||
| use std::io::prelude::*; | ||||
| 
 | ||||
|  | @ -57,6 +59,30 @@ fn main() { | |||
|                 Err(e) => eprintln!("Error: Failed to write diary entry: {e}"), | ||||
|             }; | ||||
|         } | ||||
|         args::Commands::Inventory => { | ||||
|             let config = Config::read().unwrap_or_fatal("Failed to read config file"); | ||||
|             let time = Time::build().unwrap_or_fatal("Failed to determine time information"); | ||||
|             let year = time.datetime.format("%Y").to_string(); | ||||
|             let month_name = time.datetime.format("%B").to_string(); | ||||
|             let day_ordinal = util::ordinal(time.datetime.day()); | ||||
| 
 | ||||
|             let inventory = | ||||
|                 Inventory::take(&config).unwrap_or_fatal("Failed to take inventory"); | ||||
| 
 | ||||
|             println!("*** Inventory ***"); | ||||
|             println!( | ||||
|                 "For the {day_ordinal} of {month_name}, {year} in {}", | ||||
|                 time.timezone | ||||
|             ); | ||||
|             println!(); | ||||
|             println!("You have {} diary entries so far.", inventory.entries); | ||||
|             println!( | ||||
|                 "You have written {} words so far, with an average of {} words per entry.", | ||||
|                 inventory.word_count, inventory.avg_word_count | ||||
|             ); | ||||
|             println!(); | ||||
|             println!("*** Happy journaling! ***") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										115
									
								
								src/mgmt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/mgmt.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | |||
| use diaryrs::{macros::UnwrapOrFatalAble, time::Time}; | ||||
| use regex::Regex; | ||||
| 
 | ||||
| use crate::args::Config; | ||||
| 
 | ||||
| use std::{fs, path}; | ||||
| 
 | ||||
| // Management for existing diary entries
 | ||||
| 
 | ||||
| /// Overall stats and information about existing diary entries
 | ||||
| ///
 | ||||
| /// Call `::take` to construct an `Inventory` struct from diary entires on disk
 | ||||
| #[derive(Debug)] | ||||
| pub struct Inventory { | ||||
|     /// Number of entries in the diary
 | ||||
|     pub entries: usize, | ||||
| 
 | ||||
|     /// Total word count of all entries
 | ||||
|     ///
 | ||||
|     /// Does not include YAML front matter or comments
 | ||||
|     pub word_count: u64, | ||||
| 
 | ||||
|     /// Average word count: simply `word_count / entries` rounded to the nearest integer
 | ||||
|     pub avg_word_count: f64, | ||||
| } | ||||
| 
 | ||||
| impl Inventory { | ||||
|     /// Take inventory of the diary entries
 | ||||
|     ///
 | ||||
|     /// Reads the diary entries from disks and produces a useful
 | ||||
|     /// `Inventory` struct with stats and information about the entries.
 | ||||
|     pub fn take(config: &Config) -> Result<Self, Box<dyn std::error::Error>> { | ||||
|         // Take directory listing
 | ||||
|         let dir = fs::read_dir(&config.base_path)?; | ||||
| 
 | ||||
|         let md_paths: Vec<_> = dir | ||||
|             .filter_map(|entry| entry.ok()) | ||||
|             .flat_map(|entry| recurse_paths_md(entry)) | ||||
|             .flatten() | ||||
|             .collect(); | ||||
| 
 | ||||
|         let word_count = word_count_mds(&md_paths)?; | ||||
|         let entries_count = md_paths.len(); | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             entries: entries_count, | ||||
|             word_count, | ||||
|             avg_word_count: (word_count as f64 / entries_count as f64).round(), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Recursively finds every Markdown (*.md) file in a directory
 | ||||
| /// and its subdirectories
 | ||||
| ///
 | ||||
| /// Returns a vector of `PathBuf`s to each Markdown file or a boxed `Error`.
 | ||||
| fn recurse_paths_md(dir: fs::DirEntry) -> Result<Vec<path::PathBuf>, Box<dyn std::error::Error>> { | ||||
|     let mut paths = Vec::new(); | ||||
|     let current_path = dir.path(); | ||||
|     let path_type = dir.file_type()?; | ||||
| 
 | ||||
|     if path_type.is_dir() { | ||||
|         // List dir, then run this function on each entry
 | ||||
|         // Add anything returned to the paths vector
 | ||||
|         let dir = fs::read_dir(¤t_path)?; | ||||
|         paths.extend( | ||||
|             dir.filter_map(|entry| entry.ok()) | ||||
|                 .flat_map(|entry: fs::DirEntry| recurse_paths_md(entry)) | ||||
|                 .flatten(), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     if path_type.is_file() { | ||||
|         // Check if file is an md file
 | ||||
|         if let Some(ext) = current_path.extension() { | ||||
|             if ext == "md" { | ||||
|                 paths.push(current_path); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     Ok(paths) | ||||
| } | ||||
| 
 | ||||
| fn word_count_mds(paths: &Vec<path::PathBuf>) -> Result<u64, Box<dyn std::error::Error>> { | ||||
|     // temp regexes
 | ||||
|     let re_titles = Regex::new(r"(?m)^#{1,6} .+") | ||||
|         .unwrap_or_fatal("Failed to compile title regex. Something is very wrong!"); | ||||
|     let re_comments = Regex::new(r"(?s-m)<!---?.+-->") | ||||
|         .unwrap_or_fatal("Failed to compile comment regex. Something is very wrong!"); | ||||
|     let re_images = Regex::new(r"!\[.*\]\(.+\)") | ||||
|         .unwrap_or_fatal("Failed to compile image regex. Something is very wrong!"); | ||||
| 
 | ||||
|     let mut word_count = 0; | ||||
| 
 | ||||
|     for path in paths { | ||||
|         let contents = fs::read_to_string(&path)?; | ||||
| 
 | ||||
|         // Cut YAML header and comments
 | ||||
|         let contents = &re_titles.replace_all( | ||||
|             contents | ||||
|                 .split("---") | ||||
|                 .collect::<Vec<&str>>() | ||||
|                 .pop() | ||||
|                 .ok_or("No content found")?, | ||||
|             "", | ||||
|         ); | ||||
|         let contents = &re_comments.replace_all(contents, ""); | ||||
|         let contents = &re_images.replace_all(contents, ""); | ||||
| 
 | ||||
|         word_count += contents.split_whitespace().count(); | ||||
|     } | ||||
| 
 | ||||
|     Ok(word_count as u64) | ||||
| } | ||||
		Loading…
	
		Reference in a new issue