generated from Astrial.org/template
Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
2f78459ef6 | |||
375d547bc1 | |||
c97472914f | |||
6a3f3cee9d | |||
7241a1e8cd | |||
a310bfde34 |
6 changed files with 141 additions and 29 deletions
|
@ -12,6 +12,8 @@ diaryrs
|
||||||
deserialised
|
deserialised
|
||||||
indoc
|
indoc
|
||||||
mgmt
|
mgmt
|
||||||
|
tzdb
|
||||||
|
serde
|
||||||
|
|
||||||
# Diary Entries
|
# Diary Entries
|
||||||
Shunde
|
Shunde
|
||||||
|
|
32
Cargo.lock
generated
32
Cargo.lock
generated
|
@ -200,6 +200,7 @@ dependencies = [
|
||||||
"indoc",
|
"indoc",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -266,6 +267,12 @@ version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.76"
|
version = "0.3.76"
|
||||||
|
@ -418,6 +425,12 @@ version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.216"
|
version = "1.0.216"
|
||||||
|
@ -447,6 +460,19 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -516,6 +542,12 @@ version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
|
@ -11,4 +11,5 @@ iana-time-zone = "0.1.61"
|
||||||
indoc = "2.0.5"
|
indoc = "2.0.5"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
serde = { version = "1.0.216", features = ["derive"] }
|
serde = { version = "1.0.216", features = ["derive"] }
|
||||||
|
serde_yaml = { version = "0.9.34" }
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub struct Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
const CONFIG_PATHS: [&str; 5] = [
|
const CONFIG_PATHS: [&'static str; 5] = [
|
||||||
"diaryrs.toml",
|
"diaryrs.toml",
|
||||||
"diary.toml",
|
"diary.toml",
|
||||||
"~/.config/diaryrs.toml",
|
"~/.config/diaryrs.toml",
|
||||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -68,19 +68,25 @@ fn main() {
|
||||||
|
|
||||||
let inventory = Inventory::take(&config).unwrap_or_fatal("Failed to take inventory");
|
let inventory = Inventory::take(&config).unwrap_or_fatal("Failed to take inventory");
|
||||||
|
|
||||||
|
let last_entry = inventory.entries.last();
|
||||||
|
|
||||||
println!("*** Inventory ***");
|
println!("*** Inventory ***");
|
||||||
println!(
|
println!(
|
||||||
"For the {day_ordinal} of {month_name}, {year} in {}",
|
"For the {day_ordinal} of {month_name}, {year} in {}",
|
||||||
time.timezone
|
time.timezone
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
println!("You have {} diary entries so far.", inventory.entries);
|
match last_entry {
|
||||||
|
Some(entry) => println!("Your last entry was written in {} at {}", entry.timezone, entry.timestamp.to_string()),
|
||||||
|
None => println!("Your last entry is not known!")
|
||||||
|
}
|
||||||
|
println!("You have {} diary entries so far.", inventory.entries_count);
|
||||||
println!(
|
println!(
|
||||||
"You have written {} words so far, with an average of {} words per entry.",
|
"You have written {} words so far, with an average of {} words per entry.",
|
||||||
inventory.word_count, inventory.avg_word_count
|
inventory.word_count, inventory.avg_word_count
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
println!("*** Happy journaling! ***")
|
println!("*** Happy journaling! ***");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
123
src/mgmt.rs
123
src/mgmt.rs
|
@ -1,11 +1,14 @@
|
||||||
|
use chrono::FixedOffset;
|
||||||
use diaryrs::macros::UnwrapOrFatalAble;
|
use diaryrs::macros::UnwrapOrFatalAble;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::args::Config;
|
use crate::args::Config;
|
||||||
|
|
||||||
use std::{fs, path};
|
use std::{fs, path};
|
||||||
|
|
||||||
// Management for existing diary entries
|
// Management for existing diary entries
|
||||||
|
// TODO: Proper error handling for all the times we used `?`
|
||||||
|
|
||||||
/// Overall stats and information about existing diary entries
|
/// Overall stats and information about existing diary entries
|
||||||
///
|
///
|
||||||
|
@ -13,7 +16,12 @@ use std::{fs, path};
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Inventory {
|
pub struct Inventory {
|
||||||
/// Number of entries in the diary
|
/// Number of entries in the diary
|
||||||
pub entries: usize,
|
pub entries_count: usize,
|
||||||
|
|
||||||
|
/// A vector of [`Entry`] representing each diary entry
|
||||||
|
///
|
||||||
|
/// Sorted oldest to newest based on enclosed timestamp
|
||||||
|
pub entries: Vec<Entry>,
|
||||||
|
|
||||||
/// Total word count of all entries
|
/// Total word count of all entries
|
||||||
///
|
///
|
||||||
|
@ -24,6 +32,68 @@ pub struct Inventory {
|
||||||
pub avg_word_count: f64,
|
pub avg_word_count: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the front-matter metadata included within each entry. Does not necessarily have to be associated with any real diary entry.
|
||||||
|
///
|
||||||
|
/// "Lower-level" (more representative of what is on disk) than an [`Entry`]. For example, it does not do any validation or processing of the timezone
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Metadata {
|
||||||
|
timestamp: String,
|
||||||
|
timezone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
pub fn read_from_entry(contents: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
// Match YAML section
|
||||||
|
let yaml_regex = Regex::new(r"(?U)---\n([\S\s]+)---\n")
|
||||||
|
.unwrap_or_fatal("Failed to compile YAML regex. Something is very wrong!");
|
||||||
|
|
||||||
|
let matches = yaml_regex
|
||||||
|
.captures(&contents)
|
||||||
|
.ok_or("YAML front-matter not found!")?;
|
||||||
|
|
||||||
|
let yaml_str = matches
|
||||||
|
.get(1)
|
||||||
|
.ok_or("YAML front-matter match not found!")?
|
||||||
|
.as_str();
|
||||||
|
|
||||||
|
let meta = serde_yaml::from_str::<Metadata>(yaml_str)?;
|
||||||
|
|
||||||
|
Ok(meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a single diary entry (markdown file) and its associated stats/metadata
|
||||||
|
///
|
||||||
|
/// Does not include the actual contents of the entry, only stats
|
||||||
|
///
|
||||||
|
/// The offset of this entry can be retrieved from the timestamp. The tzdb-style timezone (e.g. `Australia/Sydney`)
|
||||||
|
/// is separately specified. Using the offset is probably preferred as the offset should already account for regional daylight saving and other oddities.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub path: path::PathBuf,
|
||||||
|
|
||||||
|
pub word_count: u64,
|
||||||
|
pub timestamp: chrono::DateTime<FixedOffset>,
|
||||||
|
pub timezone: chrono_tz::Tz,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
pub fn read(path: path::PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let contents = fs::read_to_string(&path)?;
|
||||||
|
|
||||||
|
let meta = Metadata::read_from_entry(&contents)?;
|
||||||
|
let timestamp = chrono::DateTime::parse_from_rfc3339(&meta.timestamp)?;
|
||||||
|
let timezone: chrono_tz::Tz = meta.timezone[0..meta.timezone.len() - 8].parse()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
path,
|
||||||
|
word_count: count_words(&contents)?,
|
||||||
|
timestamp,
|
||||||
|
timezone,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Inventory {
|
impl Inventory {
|
||||||
/// Take inventory of the diary entries
|
/// Take inventory of the diary entries
|
||||||
///
|
///
|
||||||
|
@ -39,11 +109,21 @@ impl Inventory {
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let word_count = word_count_mds(&md_paths)?;
|
let mut entries = Vec::new();
|
||||||
let entries_count = md_paths.len();
|
|
||||||
|
for path in md_paths {
|
||||||
|
entries.push(Entry::read(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort entry oldest to newest
|
||||||
|
entries.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
||||||
|
|
||||||
|
let word_count = entries.iter().fold(0, |acc, e| acc + e.word_count);
|
||||||
|
let entries_count = entries.len();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
entries: entries_count,
|
entries_count,
|
||||||
|
entries,
|
||||||
word_count,
|
word_count,
|
||||||
avg_word_count: (word_count as f64 / entries_count as f64).round(),
|
avg_word_count: (word_count as f64 / entries_count as f64).round(),
|
||||||
})
|
})
|
||||||
|
@ -82,8 +162,7 @@ fn recurse_paths_md(dir: fs::DirEntry) -> Result<Vec<path::PathBuf>, Box<dyn std
|
||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn word_count_mds(paths: &Vec<path::PathBuf>) -> Result<u64, Box<dyn std::error::Error>> {
|
fn count_words(contents: &str) -> Result<u64, Box<dyn std::error::Error>> {
|
||||||
// temp regexes
|
|
||||||
let re_titles = Regex::new(r"(?m)^#{1,6} .+")
|
let re_titles = Regex::new(r"(?m)^#{1,6} .+")
|
||||||
.unwrap_or_fatal("Failed to compile title regex. Something is very wrong!");
|
.unwrap_or_fatal("Failed to compile title regex. Something is very wrong!");
|
||||||
let re_comments = Regex::new(r"(?s-m)<!---?.+-->")
|
let re_comments = Regex::new(r"(?s-m)<!---?.+-->")
|
||||||
|
@ -91,25 +170,17 @@ fn word_count_mds(paths: &Vec<path::PathBuf>) -> Result<u64, Box<dyn std::error:
|
||||||
let re_images = Regex::new(r"!\[.*\]\(.+\)")
|
let re_images = Regex::new(r"!\[.*\]\(.+\)")
|
||||||
.unwrap_or_fatal("Failed to compile image regex. Something is very wrong!");
|
.unwrap_or_fatal("Failed to compile image regex. Something is very wrong!");
|
||||||
|
|
||||||
let mut word_count = 0;
|
// Cut YAML header, comments and images
|
||||||
|
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, "");
|
||||||
|
|
||||||
for path in paths {
|
Ok(contents.split_whitespace().count() as u64)
|
||||||
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