diff --git a/.env.dist b/.env.dist index 30b0dae..ffb190f 100644 --- a/.env.dist +++ b/.env.dist @@ -1,2 +1,3 @@ NOST_WORK_CURRENCY=EUR RUST_LOG=debug +NOT_PATH=/your-not-path/not \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 26b0b51..1aebbcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,6 +361,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "jiff" version = "0.2.16" @@ -436,7 +442,7 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "nost" -version = "0.1.0" +version = "0.2.1" dependencies = [ "chrono", "dotenv", @@ -445,6 +451,7 @@ dependencies = [ "log", "regex", "serde", + "serde_json", "serial_test", "tempfile", "toml", @@ -639,6 +646,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1117,3 +1137,9 @@ checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index ea116e2..a0ea0f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,9 @@ [package] name = "nost" -version = "0.1.0" +version = "0.2.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] regex = "1" dotenv = "0.15.0" @@ -13,10 +12,11 @@ log = "0.4.28" env_logger = "0.11.8" toml = "0.8" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.149" [dependencies.uuid] version = "1.17.0" -# Lets you generate random UUIDs +# generate random UUIDs features = [ "v4", ] diff --git a/README.md b/README.md index fd7f5b2..2882294 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ Copy `config.toml.dist` into `config.toml` and update the values. For example: ```toml not_path="/home/gaetan/not" language="fr" -log_level="debug" ``` ## Build the app diff --git a/config.toml.dist b/config.toml.dist index e82060b..1afcb0f 100644 --- a/config.toml.dist +++ b/config.toml.dist @@ -1,4 +1,3 @@ not_path="" # optionnal, default is 'en', only 'en' and 'fr' are supported for now language="en" -log_level="debug" diff --git a/src/annotations/annotate.rs b/src/annotations/annotate.rs index 77a75a0..18923aa 100644 --- a/src/annotations/annotate.rs +++ b/src/annotations/annotate.rs @@ -1,13 +1,13 @@ use uuid::Uuid; use crate::{ - dates::get::get_now_as_string, events::models::NotEvent, files::append::append, + dates::get::get_now_as_string, events::models::EventName, files::append::append, files::check_content::ends_with_line_break, }; pub fn annotate( date: Option<&str>, - event: NotEvent, + event: EventName, input_uid: Option<&Uuid>, not_path: &str, workday: Option<&str>, @@ -73,7 +73,7 @@ mod tests { // Call annotate super::annotate( None, - crate::events::models::NotEvent::CreateNot, + crate::events::models::EventName::CreateNot, None, file_path.to_str().unwrap(), None, diff --git a/src/annotations/filter.rs b/src/annotations/filter.rs index f8c4263..c0ca070 100644 --- a/src/annotations/filter.rs +++ b/src/annotations/filter.rs @@ -1,8 +1,8 @@ -use crate::{annotations::models::Annotation, events::models::NotEvent}; +use crate::{annotations::models::Annotation, events::models::EventName}; pub fn filter_annotation_by_events( annotations: Vec, - event: Vec, + event: Vec, ) -> Vec { annotations .into_iter() diff --git a/src/annotations/models.rs b/src/annotations/models.rs index ff04610..74541da 100644 --- a/src/annotations/models.rs +++ b/src/annotations/models.rs @@ -1,11 +1,11 @@ -use crate::events::models::NotEvent; +use crate::events::models::EventName; use chrono::{DateTime, FixedOffset}; use uuid::Uuid; #[derive(Debug, Clone)] pub struct Annotation { pub _uid: Uuid, - pub event: NotEvent, + pub event: EventName, pub datetime: DateTime, // todo: remove from core, should be plugin specific pub workday: Option, diff --git a/src/annotations/parse.rs b/src/annotations/parse.rs index a5e4b02..75a01df 100644 --- a/src/annotations/parse.rs +++ b/src/annotations/parse.rs @@ -4,7 +4,7 @@ use uuid::Uuid; use crate::{ annotations::{extract::extract_field_from_annotation, models::Annotation}, - events::models::NotEvent, + events::models::EventName, }; pub fn parse_annotation(annotation_in_text: &str) -> Result { @@ -15,7 +15,7 @@ pub fn parse_annotation(annotation_in_text: &str) -> Result { // extract event let event = extract_field_from_annotation(annotation_in_text, "event") - .and_then(|event_str| NotEvent::from_str(&event_str).ok()) + .and_then(|event_str| EventName::from_str(&event_str).ok()) .ok_or("Missing or invalid event")?; // extract uid @@ -46,7 +46,10 @@ mod tests { annotation.datetime.to_rfc3339(), "2025-09-29T00:00:43+02:00" ); - assert_eq!(annotation.event, crate::events::models::NotEvent::StartWork); + assert_eq!( + annotation.event, + crate::events::models::EventName::StartWork + ); assert_eq!( annotation._uid.to_string(), "b86bc6ed-50a5-4ef2-bdd3-e17baef11eff" diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 9d52a2b..519f2fe 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1 +1,2 @@ pub mod new; +pub mod new_folder; diff --git a/src/commands/new.rs b/src/commands/new.rs index 3a48bc5..5cabf57 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -1,6 +1,9 @@ -use crate::files::create::create_file; +use crate::{ + files::create::{create_file, create_note_file_with_folders}, + projects::initialize::initialize_project, +}; -pub fn new(args: Vec) { +pub fn new_legacy(args: Vec) { if args.len() > 2 { println!("Creating not with title: {}", args[1]); create_file(Some(args[2].clone())).unwrap(); @@ -10,3 +13,12 @@ pub fn new(args: Vec) { std::process::exit(0); } + +pub fn new() { + println!("Creating new note for today..."); + let _ = initialize_project(); + let _ = create_note_file_with_folders("default".to_string()); + + println!("✅ Note has been created successfully!"); + std::process::exit(0); +} diff --git a/src/commands/new_folder.rs b/src/commands/new_folder.rs new file mode 100644 index 0000000..2709540 --- /dev/null +++ b/src/commands/new_folder.rs @@ -0,0 +1,50 @@ +use std::fs::{create_dir_all, File}; + +use crate::{ + configurations::get::get_value_from_config, files::build_paths::build_folder_path_for_now, +}; + +pub fn new_folder() -> std::io::Result { + // compose path of the day + let not_path = get_value_from_config("not_path").unwrap(); + let new_folder_path = build_folder_path_for_now(¬_path); + + // create folder of the composed path (and recursive parents if needed) + match create_dir_all(&new_folder_path) { + Ok(_result) => { + println!("✅ Folder has been created successfully!"); + + // create a the config { "created_at": "2025-12-31T23:59:59Z", "type": "day" } + let config_file_path = format!("{}{}", &new_folder_path, ".not-config.json"); + + match File::create(&config_file_path) { + Ok(_file) => { + println!("✅ Config file created: {}", config_file_path); + // add minimal content to the config file + } + Err(e) => { + eprintln!("Error creating file: {}", e); + } + }; + + // create the default file in the folder + let default_file_path = format!("{}{}", &new_folder_path, "not.md"); + + match File::create(&default_file_path) { + Ok(_file) => { + println!("✅ Default file created: {}", default_file_path); + // add minimal content to the default file: the date and add an info in config file + } + Err(e) => { + eprintln!("Error creating file: {}", e); + } + }; + } + Err(e) => { + eprintln!("Error creating folder: {}", e); + return Err(e); + } + } + + Ok("Nost has created the folder successfully!".to_string()) +} diff --git a/src/configurations/get.rs b/src/configurations/get.rs index 8d9b204..7312e15 100644 --- a/src/configurations/get.rs +++ b/src/configurations/get.rs @@ -12,6 +12,10 @@ pub fn get_config() -> Result> { Ok(config) } +/** + * Get a specific value from the configuration file based on the provided key. + * i.e. get_value_from_config("not_path") will return the value of the "not_path" key in the configuration file. + */ pub fn get_value_from_config(key: &str) -> Result> { let configuration = get_config().unwrap(); diff --git a/src/configurations/models.rs b/src/configurations/models.rs index 8176614..e81e485 100644 --- a/src/configurations/models.rs +++ b/src/configurations/models.rs @@ -5,19 +5,17 @@ use std::path::PathBuf; pub struct Config { pub not_path: PathBuf, pub language: String, - pub log_level: String, } impl Config { pub fn keys() -> &'static [&'static str] { - &["not_path", "language", "log_level"] + &["not_path", "language"] } pub fn get_value(&self, key: &str) -> Option { match key { "not_path" => Some(self.not_path.to_string_lossy().into_owned()), "language" => Some(self.language.clone()), - "log_level" => Some(self.log_level.clone()), _ => None, } } diff --git a/src/dates/get.rs b/src/dates/get.rs index 7b35446..48cb752 100644 --- a/src/dates/get.rs +++ b/src/dates/get.rs @@ -1,10 +1,14 @@ -use chrono::{Datelike, Local}; +use chrono::{DateTime, Datelike, Local}; pub fn get_now_as_string() -> String { - let now = Local::now(); + let now: DateTime = Local::now(); format!("{}{}", now.format("%Y-%m-%dT%H:%M:%S"), now.format("%:z")) } +pub fn get_day_as_string(datetime: DateTime) -> String { + format!("{}", datetime.format("%d")) +} + pub fn get_week_of_month() -> u32 { let today = chrono::Local::now().date_naive(); diff --git a/src/events/mod.rs b/src/events/mod.rs index c446ac8..aca4e55 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -1 +1,2 @@ pub mod models; +pub mod record; diff --git a/src/events/models.rs b/src/events/models.rs index 88f9246..4e0b77b 100644 --- a/src/events/models.rs +++ b/src/events/models.rs @@ -1,32 +1,55 @@ +use chrono::Local; +use serde::{Deserialize, Serialize}; use std::fmt; +use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum NotEvent { - // todo: move in work event enum +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum EventName { StartWork, StopWork, - // keep in core CreateNot, } -impl fmt::Display for NotEvent { +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Event { + pub datetime: String, + pub event: String, + pub day: String, + pub not_type: String, + pub uid: String, +} + +impl Event { + pub fn now(event_name: EventName, not_type: String) -> Self { + let now = Local::now(); + Self { + datetime: now.to_rfc3339(), + event: format!("{}", event_name), + day: now.format("%Y-%m-%d").to_string(), + not_type, + uid: Uuid::new_v4().to_string(), + } + } +} + +impl fmt::Display for EventName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - NotEvent::StartWork => write!(f, "START_WORK"), - NotEvent::StopWork => write!(f, "STOP_WORK"), - NotEvent::CreateNot => write!(f, "CREATE_NOT"), + EventName::StartWork => write!(f, "START_WORK"), + EventName::StopWork => write!(f, "STOP_WORK"), + EventName::CreateNot => write!(f, "CREATE_NOT"), } } } -impl std::str::FromStr for NotEvent { +impl std::str::FromStr for EventName { type Err = (); fn from_str(s: &str) -> std::result::Result { match s { - "START_WORK" => Ok(NotEvent::StartWork), - "STOP_WORK" => Ok(NotEvent::StopWork), - "CREATE_NOT" => Ok(NotEvent::CreateNot), + "START_WORK" => Ok(EventName::StartWork), + "STOP_WORK" => Ok(EventName::StopWork), + "CREATE_NOT" => Ok(EventName::CreateNot), _ => Err(()), } } diff --git a/src/events/record.rs b/src/events/record.rs new file mode 100644 index 0000000..83c4112 --- /dev/null +++ b/src/events/record.rs @@ -0,0 +1,86 @@ +use std::{ + fs::{create_dir_all, read_to_string, write, File}, + io::Error, + path::Path, +}; + +use log::debug; + +use crate::{events::models::Event, projects::initialize::get_project_config_path}; + +pub fn record_event(event: Event) -> std::io::Result { + let config_path = get_project_config_path(); + debug!("Project config path: {:?}", config_path); + + // create journal folder if not exists + if let Err(e) = create_dir_all(&config_path) { + return Err(Error::other(format!( + "🛑 Failed to create directory: {}", + e + ))); + } + + // create journal file if not exists + let journal_file_path = format!("{}/journal.json", config_path); + if !Path::new(&journal_file_path).exists() { + if let Err(e) = File::create(&journal_file_path) { + return Err(Error::other(format!( + "🛑 Failed to create journal file: {}", + e + ))); + } + + // initialize the journal file with an empty array + if let Err(e) = write(&journal_file_path, "[]") { + return Err(Error::other(format!( + "🛑 Failed to initialize journal file: {}", + e + ))); + } + } + + // create the record + let record = serde_json::to_value(&event) + .map_err(|e| Error::other(format!("🛑 Failed to serialize event record: {}", e)))?; + + // append the record in the array (at the end) + let journal_content = read_to_string(&journal_file_path).map_err(|e| { + Error::other(format!( + "🛑 Failed to read journal file '{}': {}", + journal_file_path, e + )) + })?; + + let mut journal_json: serde_json::Value = + serde_json::from_str(&journal_content).map_err(|e| { + Error::other(format!( + "🛑 Invalid JSON in journal file '{}': {}", + journal_file_path, e + )) + })?; + + let journal_array = journal_json.as_array_mut().ok_or_else(|| { + Error::other(format!( + "🛑 Journal file '{}' must contain a JSON array", + journal_file_path + )) + })?; + + journal_array.push(record); + + let updated_content = serde_json::to_string_pretty(&journal_json).map_err(|e| { + Error::other(format!( + "🛑 Failed to serialize updated journal '{}': {}", + journal_file_path, e + )) + })?; + + write(&journal_file_path, format!("{}\n", updated_content)).map_err(|e| { + Error::other(format!( + "🛑 Failed to write updated journal '{}': {}", + journal_file_path, e + )) + })?; + + Ok("Record has been added.".to_string()) +} diff --git a/src/files/build_paths.rs b/src/files/build_paths.rs index b401007..f6d5389 100644 --- a/src/files/build_paths.rs +++ b/src/files/build_paths.rs @@ -16,3 +16,18 @@ pub fn build_file_path_for_now(base_path: &str) -> String { format!("{}/{}/{}/{}/", base_path, year, month, get_week_of_month()) } + +pub fn build_folder_path_for_now(base_path: &str) -> String { + let today = Local::now().date_naive(); + let year = today.year(); + let month = format!("{:02}", today.month()); + + format!( + "{}/{}/{}/{}/{}/", + base_path, + year, + month, + get_week_of_month(), + today.day() + ) +} diff --git a/src/files/create.rs b/src/files/create.rs index 2196e67..a1d8663 100644 --- a/src/files/create.rs +++ b/src/files/create.rs @@ -4,12 +4,21 @@ use std::{ path::Path, }; +use chrono::{DateTime, Local}; + use crate::{ annotations::annotate::annotate, configurations::get::get_value_from_config, - dates::get::{get_date_as_text_en, get_date_as_text_fr}, - events::models::NotEvent, - files::{append::append, build_paths::build_file_path_for_now, name::name}, + dates::get::{get_date_as_text_en, get_date_as_text_fr, get_day_as_string}, + events::{ + models::{Event, EventName}, + record::record_event, + }, + files::{ + append::append, + build_paths::{build_file_path_for_now, build_folder_path_for_now}, + name::name, + }, }; pub fn create_file(title: Option) -> std::io::Result { @@ -50,7 +59,7 @@ pub fn create_file(title: Option) -> std::io::Result { annotate( None, - NotEvent::CreateNot, + EventName::CreateNot, None, full_not_file_path.as_str(), None, @@ -68,3 +77,62 @@ pub fn create_file(title: Option) -> std::io::Result { Ok(full_not_file_path) } + +pub fn create_note_file_with_folders(note_type: String) -> std::io::Result { + // get the path of the folder to create + let not_path = get_value_from_config("not_path").unwrap(); + let today_folder_path = build_folder_path_for_now(¬_path); + + log::debug!( + "🚨 Creating note file with folders at path: {}", + &today_folder_path + ); + + let now: DateTime = Local::now(); + let today_file_name = get_day_as_string(now); + let today_file_path = format!( + "{}{}{}{}{}", + &today_folder_path, today_file_name, ".", note_type, ".md" + ); + + // only create if not does not already exists + if Path::new(&today_file_path).exists() { + println!("Not already existed."); + return Ok(today_file_path); + } + + // create folders if needed + if let Err(e) = create_dir_all(&today_folder_path) { + return Err(Error::other(format!( + "🛑 Failed to create directory: {}", + e + ))); + } + + log::debug!( + "🚨 Creating note file with folders at path: {}", + &today_file_path + ); + + // create the file + match File::create(&today_file_path) { + Ok(_file) => { + record_event(Event::now(EventName::CreateNot, note_type.clone()))?; + println!("✅ File created: {}", today_file_path); + } + Err(e) => { + eprintln!("Error creating file: {}", e); + } + }; + + let date_line = match get_value_from_config("language").unwrap().as_str() { + "fr" => get_date_as_text_fr(), + _ => get_date_as_text_en(), // default to English + }; + + append(today_file_path.clone().into(), &date_line).expect("🛑 Failed to append date as text."); + + println!("✅ New \"not\" has successfully being initiated."); + + Ok(today_file_path) +} diff --git a/src/files/find.rs b/src/files/find.rs index 90e068d..b7660ef 100644 --- a/src/files/find.rs +++ b/src/files/find.rs @@ -47,7 +47,6 @@ pub fn get_current_directory() -> Result> { let execution_dir = execution_path .parent() .ok_or("Could not determine executable directory")?; - println!("Execution directory: {:?}\n", execution_dir); Ok(execution_dir.to_path_buf()) } diff --git a/src/main.rs b/src/main.rs index 0c30906..4a7349f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ mod dates; mod events; mod files; mod plugins; -use crate::commands::new::new; +mod projects; +use crate::commands::new::{new, new_legacy}; use crate::plugins::gdarquie_work::commands::end_work::end_work; use crate::plugins::gdarquie_work::commands::start_work::start_work; use crate::plugins::gdarquie_work::commands::work_stats::work_stats; @@ -26,13 +27,21 @@ fn main() { } if args[1] == "new" || args[1] == "n" { - new(args); + new_legacy(args); } else if args[1] == "start-work" || args[1] == "sw" { start_work(args); } else if args[1] == "end-work" || args[1] == "ew" { end_work(); } else if args[1] == "work-stats" || args[1] == "ws" { work_stats(args); + } else if args[1] == "new-folder" || args[1] == "nf" { + let _ = commands::new_folder::new_folder(); + } else if args[1] == "new-default" || args[1] == "nn" { + // wip + new(); + println!("Creating new default note..."); + } else if args[1] == "new-start-work" { + // implement new start work } else { eprintln!("Unknown command: \"{}\"", args[1]); std::process::exit(1); diff --git a/src/plugins/gdarquie_work/commands/end_work.rs b/src/plugins/gdarquie_work/commands/end_work.rs index ccf5887..4242299 100644 --- a/src/plugins/gdarquie_work/commands/end_work.rs +++ b/src/plugins/gdarquie_work/commands/end_work.rs @@ -2,7 +2,7 @@ use std::path::Path; use crate::{ annotations::annotate::annotate, - events::models::NotEvent, + events::models::EventName, files::create::create_file, plugins::gdarquie_work::work_annotations::{ find::find_last_work_annotation, models::WorkAnnotationWithPath, @@ -46,7 +46,7 @@ fn add_stop_work_annotations(last_annotation: &WorkAnnotationWithPath, path: &Pa // STOP_WORK for yesterday annotate( Some(&yesterday_datetime_string), - NotEvent::StopWork, + EventName::StopWork, None, last_annotation.path.to_str().unwrap(), last_annotation.annotation.workday.as_deref(), @@ -61,7 +61,7 @@ fn add_stop_work_annotations(last_annotation: &WorkAnnotationWithPath, path: &Pa // START_WORK for today annotate( Some(&today_datetime_string), - NotEvent::StartWork, + EventName::StartWork, None, path.to_str().unwrap(), last_annotation.annotation.workday.as_deref(), @@ -72,7 +72,7 @@ fn add_stop_work_annotations(last_annotation: &WorkAnnotationWithPath, path: &Pa // we add a STOP_WORK annotation for today annotate( None, - NotEvent::StopWork, + EventName::StopWork, None, path.to_str().unwrap(), last_annotation.annotation.workday.as_deref(), @@ -80,7 +80,7 @@ fn add_stop_work_annotations(last_annotation: &WorkAnnotationWithPath, path: &Pa } pub fn has_active_session(last_work_annotation: &WorkAnnotationWithPath) -> bool { - if last_work_annotation.annotation.event == NotEvent::StartWork { + if last_work_annotation.annotation.event == EventName::StartWork { return true; } diff --git a/src/plugins/gdarquie_work/commands/start_work.rs b/src/plugins/gdarquie_work/commands/start_work.rs index 14022a9..64ef43e 100644 --- a/src/plugins/gdarquie_work/commands/start_work.rs +++ b/src/plugins/gdarquie_work/commands/start_work.rs @@ -1,5 +1,5 @@ use crate::{ - annotations::annotate::annotate, events::models::NotEvent, files::create::create_file, + annotations::annotate::annotate, events::models::EventName, files::create::create_file, }; pub fn start_work(args: Vec) { @@ -16,6 +16,6 @@ pub fn start_work(args: Vec) { default_workday = chrono::Local::now().format("%Y-%m-%d").to_string(); Some(default_workday.as_str()) }; - annotate(None, NotEvent::StartWork, None, ¬_path, workday); + annotate(None, EventName::StartWork, None, ¬_path, workday); std::process::exit(0); } diff --git a/src/plugins/gdarquie_work/work.rs b/src/plugins/gdarquie_work/work.rs index 06f28d7..5598728 100644 --- a/src/plugins/gdarquie_work/work.rs +++ b/src/plugins/gdarquie_work/work.rs @@ -2,7 +2,7 @@ use crate::annotations::extract::extract_annotations_from_path; use crate::annotations::filter::filter_annotation_by_events; use crate::annotations::models::Annotation; use crate::configurations::get::get_value_from_config; -use crate::events::models::NotEvent; +use crate::events::models::EventName; use crate::files::build_paths::build_file_path_for_month; use chrono::Datelike; use chrono::Local; @@ -43,10 +43,10 @@ pub fn compute_work_time_from_annotations(annotations: &Vec) -> i32 for annotation in annotations { match annotation.event { - NotEvent::StartWork => { + EventName::StartWork => { start_time = Some(annotation.datetime); } - NotEvent::StopWork => { + EventName::StopWork => { if let Some(start) = start_time { total_time_in_minutes += (annotation.datetime - start).num_minutes() as i32; start_time = None; @@ -90,7 +90,7 @@ pub fn compute_monthly_work_stats(month: Option<&str>) -> Result> = HashMap::new(); @@ -233,14 +233,14 @@ mod tests { let stop = start + Duration::hours(1); let start_annotation = Annotation { _uid: Uuid::new_v4(), - event: NotEvent::StartWork, + event: EventName::StartWork, datetime: start, workday: None, }; let stop_annotation = Annotation { _uid: Uuid::new_v4(), - event: NotEvent::StopWork, + event: EventName::StopWork, datetime: stop, workday: None, }; @@ -248,7 +248,7 @@ mod tests { assert_eq!(compute_work_time_from_annotations(&annotations), 60); } - fn make_annotation(event: NotEvent, datetime: chrono::DateTime) -> Annotation { + fn make_annotation(event: EventName, datetime: chrono::DateTime) -> Annotation { Annotation { _uid: Uuid::new_v4(), event, @@ -316,8 +316,8 @@ mod tests { let start = tz.with_ymd_and_hms(2025, 9, 1, 9, 0, 0).unwrap(); let stop = start + Duration::hours(1); let annotations = vec![ - make_annotation(NotEvent::StartWork, start), - make_annotation(NotEvent::StopWork, stop), + make_annotation(EventName::StartWork, start), + make_annotation(EventName::StopWork, stop), ]; let stats = compute_work_stats_from_annotations(annotations); assert_eq!(stats.total_duration_in_minutes, 60); @@ -336,10 +336,10 @@ mod tests { let start2 = tz.with_ymd_and_hms(2025, 9, 2, 10, 0, 0).unwrap(); let stop2 = start2 + Duration::hours(2); let annotations = vec![ - make_annotation(NotEvent::StartWork, start1), - make_annotation(NotEvent::StopWork, stop1), - make_annotation(NotEvent::StartWork, start2), - make_annotation(NotEvent::StopWork, stop2), + make_annotation(EventName::StartWork, start1), + make_annotation(EventName::StopWork, stop1), + make_annotation(EventName::StartWork, start2), + make_annotation(EventName::StopWork, stop2), ]; let stats = compute_work_stats_from_annotations(annotations); assert_eq!(stats.total_duration_in_minutes, 180); @@ -364,10 +364,10 @@ mod tests { let start2 = tz.with_ymd_and_hms(2025, 9, 1, 10, 0, 0).unwrap(); // week 36 let stop2 = start2 + Duration::hours(2); let annotations = vec![ - make_annotation(NotEvent::StartWork, start1), - make_annotation(NotEvent::StopWork, stop1), - make_annotation(NotEvent::StartWork, start2), - make_annotation(NotEvent::StopWork, stop2), + make_annotation(EventName::StartWork, start1), + make_annotation(EventName::StopWork, stop1), + make_annotation(EventName::StartWork, start2), + make_annotation(EventName::StopWork, stop2), ]; let stats = compute_work_stats_from_annotations(annotations); assert_eq!(stats.total_duration_in_minutes, 180); diff --git a/src/plugins/gdarquie_work/work_annotations/find.rs b/src/plugins/gdarquie_work/work_annotations/find.rs index 6bd7242..a6d2809 100644 --- a/src/plugins/gdarquie_work/work_annotations/find.rs +++ b/src/plugins/gdarquie_work/work_annotations/find.rs @@ -1,7 +1,7 @@ use crate::annotations::extract::extract_annotations_from_path; use crate::annotations::filter::filter_annotation_by_events; use crate::configurations::get::get_value_from_config; -use crate::events::models::NotEvent; +use crate::events::models::EventName; use crate::files::find::find_all_not_files; use crate::plugins::gdarquie_work::work_annotations::models::WorkAnnotationWithPath; @@ -24,7 +24,7 @@ pub fn find_last_work_annotation() -> Option { // Check if there are any work-related annotations let mut work_annotations = filter_annotation_by_events( annotations, - vec![NotEvent::StartWork, NotEvent::StopWork], + vec![EventName::StartWork, EventName::StopWork], ); if !work_annotations.is_empty() { diff --git a/src/projects/initialize.rs b/src/projects/initialize.rs new file mode 100644 index 0000000..5d0f12f --- /dev/null +++ b/src/projects/initialize.rs @@ -0,0 +1,77 @@ +use serde_json::{json, Value}; +use std::{env, fs::create_dir_all, path::Path}; + +use crate::dates::get::get_now_as_string; + +pub fn get_project_config_path() -> String { + // compose configuration path and create configuration folder + let not_path = env::var("NOT_PATH").unwrap_or_else(|_| { + eprintln!("NOT_PATH environment variable not set."); + std::process::exit(1); + }); + format!("{}/{}/", ¬_path, ".nost") +} + +/** + * Checks if the configuration file `project.json` exists + * in the configuration directory in root/.nost + */ +pub fn is_project_initialized() -> bool { + let not_path = env::var("NOT_PATH").unwrap_or_else(|_| { + eprintln!("NOT_PATH environment variable not set."); + std::process::exit(1); + }); + + let project_config_path = format!("{}/.nost/project.json", ¬_path,); + log::debug!( + "Checking if configuration exists at path: {}", + &project_config_path + ); + Path::new(&project_config_path).is_file() +} + +/** + * Create a project.json file in the not path/.nost folder + * if it does not exist, or update the last_updated timestamp if it does. + */ +pub fn initialize_project() -> Result> { + // check if there is an existing configuration folder with a file + if is_project_initialized() { + // if this file exists update the file + let project_config_path: String = get_project_config_path(); + let config_content = std::fs::read_to_string(&project_config_path)?; + let mut config: Value = serde_json::from_str(&config_content)?; + + config["last_updated"] = json!(get_now_as_string()); + let config_file = std::fs::File::create(&project_config_path)?; + serde_json::to_writer_pretty(config_file, &config)?; + + return Ok(String::from("Configuration already exists.")); + } + + log::debug!("No configuration found. Initializing configuration..."); + let configuration_path = get_project_config_path(); + create_dir_all(&configuration_path)?; + + // create en empty configuration file + let default_config_path = format!("{}{}", &configuration_path, "project.json"); + let config_file = std::fs::File::create(&default_config_path)?; + log::debug!("Configuration initialized at path: {}", &configuration_path); + + // append inital content to the configuration file + const NOST_VERSION: &str = env!("CARGO_PKG_VERSION"); + + let initial_content = json!({ + "name": "My Project", + "description": "This is my project description.", + "created_at": get_now_as_string(), + "last_updated": get_now_as_string(), + "version": NOST_VERSION, + }); + + serde_json::to_writer_pretty(config_file, &initial_content)?; + + Ok(String::from( + "Configuration has been initialized successfully!", + )) +} diff --git a/src/projects/mod.rs b/src/projects/mod.rs new file mode 100644 index 0000000..c570ae3 --- /dev/null +++ b/src/projects/mod.rs @@ -0,0 +1 @@ +pub mod initialize;