use crate::config;
use chrono::{DateTime, Datelike, Local};
use std::fs;
use std::fs::{remove_file, File};
use std::io::prelude::*;
use std::os::unix::fs as unix_fs;
mod interpreter;
use regex::Regex;
use std::cmp;
use std::path::PathBuf;
pub fn symlink_daily_note(date: DateTime<Local>, config: &config::Config) {
println!(
"creating symlink for daily-note: {}",
date_to_file_name(date, config)
);
create_parent_dir(&config.symlink_path);
let daily_note = date_to_file_name(date, config);
let _ = remove_file(&config.symlink_path);
let _ = unix_fs::symlink(daily_note, &config.symlink_path);
}
fn create_parent_dir(path: &str) {
let path = PathBuf::from(path);
let parent_dir = path.parent().unwrap();
let _ = fs::create_dir_all(parent_dir);
}
fn date_to_file_name(date: DateTime<Local>, config: &config::Config) -> String {
let year = date.year();
let month = date.month();
let day = date.day();
let file_name = format!("{}-{:0>2}-{:0>2}.md", year, month, day);
config.archive_path.clone() + &file_name
}
pub fn create_daily_note(date: DateTime<Local>, config: &config::Config) {
let path = date_to_file_name(date, config);
create_parent_dir(&path);
if note_exists(date, config) {
println!("Daily note {} already exists", path);
return;
}
let last_note = get_latest_note(&config.archive_path);
let last_note = match last_note {
Some(note) => read_file(¬e),
None => "".to_string(),
};
let template = get_template(&config.template_path);
let note = interpreter::process_tokens(&template, &last_note, date);
let file = File::create(path);
let _ = file
.expect("File createon failed")
.write_all(note.as_bytes());
}
fn note_exists(date: DateTime<Local>, config: &config::Config) -> bool {
let note_name = date_to_file_name(date, config);
fs::exists(note_name).expect("The file system is throwing an error?")
}
fn get_latest_note(path: &str) -> Option<String> {
let files = fs::read_dir(path).ok()?;
let paths = files.map(|file| {
file.expect("File system error")
.path()
.to_string_lossy()
.to_string()
});
let daily_note_reggex = Regex::new(r".*\d{4}-\d{2}-\d{2}.md").unwrap();
let daily_notes = paths.filter(|file| daily_note_reggex.is_match(file));
let mut max = "".to_string();
for note in daily_notes {
let string_path = note;
max = cmp::max(string_path, max);
}
if max.is_empty() {
return None;
}
Some(max)
}
fn read_file(path: &str) -> String {
let file = File::open(path);
let mut contents = String::new();
let _ = file
.expect("Failed to read file {}")
.read_to_string(&mut contents);
contents
}
fn get_template(template_path: &str) -> String {
let template_exists = fs::exists(template_path).expect("The file system is throwing an error?");
if !template_exists {
create_default_template(template_path);
}
read_file(template_path).to_string()
}
fn create_default_template(template_path: &str) {
create_parent_dir(template_path);
let default_template = format!(
"
This is the default groundhog template, edit it to match your needs
To change it edit the file at {}
## Tasks
!copy_tasks",
template_path
);
fs::write(template_path, default_template).unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_dir() -> std::path::PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"groundhog-daily-note-{}-{nonce}",
std::process::id()
));
fs::create_dir_all(&path).unwrap();
path
}
fn date(year: i32, month: u32, day: u32) -> DateTime<Local> {
Local
.with_ymd_and_hms(year, month, day, 12, 0, 0)
.single()
.unwrap()
}
fn config_for(dir: &std::path::Path) -> config::Config {
config::Config {
template_path: dir.join("template.md").to_string_lossy().into_owned(),
symlink_path: dir.join("today.md").to_string_lossy().into_owned(),
archive_path: format!("{}/", dir.to_string_lossy()),
}
}
#[test]
fn date_filename_uses_zero_padded_month_and_day() {
let dir = temp_dir();
let cfg = config_for(&dir);
assert_eq!(
date_to_file_name(date(2024, 2, 3), &cfg),
format!("{}/2024-02-03.md", dir.display())
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn create_greenfield_note() {
let dir = temp_dir();
let cfg = config_for(&dir);
fs::write(&cfg.template_path, "# Work\n!copy_last_day\n").unwrap();
create_daily_note(date(2024, 2, 2), &cfg);
let expected = "# Work";
assert_eq!(
fs::read_to_string(date_to_file_name(date(2024, 2, 2), &cfg)).unwrap(),
expected
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn creates_note_using_template_and_yesterdays_section() {
let dir = temp_dir();
let cfg = config_for(&dir);
fs::write(&cfg.template_path, "# Work\n!copy_last_day\n").unwrap();
fs::write(
date_to_file_name(date(2024, 2, 1), &cfg),
"# Work\n- carry me\n",
)
.unwrap();
create_daily_note(date(2024, 2, 2), &cfg);
let expected = "# Work\n- carry me";
assert_eq!(
fs::read_to_string(date_to_file_name(date(2024, 2, 2), &cfg)).unwrap(),
expected
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn creates_note_based_on_arbitrarily_old_notes() {
let dir = temp_dir();
let cfg = config_for(&dir);
fs::write(&cfg.template_path, "# Work\n!copy_last_day\n").unwrap();
fs::write(
date_to_file_name(date(2022, 2, 1), &cfg),
"# Work\n- carry me\n",
)
.unwrap();
create_daily_note(date(2024, 2, 2), &cfg);
let expected = "# Work\n- carry me";
assert_eq!(
fs::read_to_string(date_to_file_name(date(2024, 2, 2), &cfg)).unwrap(),
expected
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn existing_note_is_not_overwritten() {
let dir = temp_dir();
let cfg = config_for(&dir);
let existing = date_to_file_name(date(2024, 2, 2), &cfg);
fs::write(&existing, "keep this note").unwrap();
create_daily_note(date(2024, 2, 2), &cfg);
assert_eq!(fs::read_to_string(existing).unwrap(), "keep this note");
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn symlink_points_to_the_requested_daily_note() {
let dir = temp_dir();
let cfg = config_for(&dir);
let note = date_to_file_name(date(2024, 2, 2), &cfg);
fs::write(¬e, "today").unwrap();
symlink_daily_note(date(2024, 2, 2), &cfg);
assert_eq!(
fs::read_link(&cfg.symlink_path).unwrap(),
std::path::PathBuf::from(note)
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn can_handle_missing_paths() {
let dir = temp_dir();
let cfg = config::Config {
template_path: dir.join("template.md").to_string_lossy().into_owned(),
symlink_path: dir
.join("aaaaa/aaaa/today.md")
.to_string_lossy()
.into_owned(),
archive_path: format!("{}/needed_path/", dir.to_string_lossy()),
};
fs::write(&cfg.template_path, "# Work\n!copy_last_day\n").unwrap();
create_daily_note(date(2024, 2, 2), &cfg);
let expected = "# Work";
assert_eq!(
fs::read_to_string(date_to_file_name(date(2024, 2, 2), &cfg)).unwrap(),
expected
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn can_handle_missing_template() {
let dir = temp_dir();
let cfg = config_for(&dir);
create_daily_note(date(2024, 2, 2), &cfg);
let expected = format!(
"This is the default groundhog template, edit it to match your needs\nTo change it edit the file at {}\n## Tasks",
cfg.template_path
);
assert_eq!(
fs::read_to_string(date_to_file_name(date(2024, 2, 2), &cfg)).unwrap(),
expected
);
fs::remove_dir_all(dir).unwrap();
}
}