diff --git a/Cargo.lock b/Cargo.lock index 55634ab..02b0f37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,18 +9,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "atty" version = "0.2.14" @@ -38,35 +26,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "base64" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" - [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -102,37 +67,20 @@ dependencies = [ ] [[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "crossbeam-utils" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" -dependencies = [ - "autocfg", - "cfg-if 1.0.0", - "lazy_static", -] - -[[package]] -name = "dirs" -version = "2.0.2" +name = "dirs-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 0.1.10", - "dirs-sys", + "cfg-if", + "dirs-sys-next", ] [[package]] -name = "dirs-sys" -version = "0.3.5" +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", @@ -147,13 +95,13 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "getrandom" -version = "0.1.16" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -162,10 +110,10 @@ version = "0.3.2" dependencies = [ "chrono", "clap", - "dirs", "libmath", "open", "rprompt", + "shellexpand", ] [[package]] @@ -177,17 +125,11 @@ dependencies = [ "libc", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.86" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" +checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a" [[package]] name = "libmath" @@ -219,9 +161,9 @@ dependencies = [ [[package]] name = "open" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e288ead50d896dde82f3c18b64e40a2bf3e941225aa5c6b35a3e8e7b6b21d6f" +checksum = "a7e9f1bdf15cd1f5a00cc9002a733a6ee6d0ff562491852d59652471c4a389f7" dependencies = [ "which", "winapi", @@ -294,19 +236,21 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.1.57" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] [[package]] name = "redox_users" -version = "0.3.5" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom", "redox_syscall", - "rust-argon2", ] [[package]] @@ -316,15 +260,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b386f4748bdae2aefc96857f5fda07647f851d089420e577831e2a14b45230f8" [[package]] -name = "rust-argon2" -version = "0.8.3" +name = "shellexpand" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829" dependencies = [ - "base64", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", + "dirs-next", ] [[package]] @@ -335,9 +276,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "syn" -version = "1.0.60" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +checksum = "8fd9bc7ccc2688b3344c2f48b9b546648b25ce0b20fc717ee7fa7981a8ca9717" dependencies = [ "proc-macro2", "quote", @@ -380,7 +321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi", "winapi", ] @@ -402,12 +343,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 51c1929..26e4edf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,12 @@ version = "0.3.2" authors = ["Sebastian Morr ", "Daryl Manning "] description = "Minimalist command line tool you can use to track and examine your habits." +edition = "2018" + [dependencies] chrono = "0.4.10" rprompt = "1.0.5" clap = "2.33.0" open = "1.3.2" -dirs = "2.0.2" libmath = "0.2.1" +shellexpand = "2.1.0" diff --git a/src/main.rs b/src/main.rs index bf592b9..9a8e041 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,9 @@ -extern crate chrono; #[macro_use] extern crate clap; -extern crate dirs; -extern crate open; -extern crate rprompt; -extern crate math; use chrono::prelude::*; use clap::{Arg, SubCommand}; +use math::round; use std::cmp; use std::collections::HashMap; use std::env; @@ -17,14 +13,36 @@ use std::fs::OpenOptions; use std::io::BufRead; use std::io::BufReader; use std::io::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process; use std::process::Command; -use math::round; + +const HABIT_FILE_TEMPLATE: &str = r"# The numbers specifies how often you want to do a habit: +# 1 means daily, 7 means weekly, 0 means you're just tracking the habit. Some examples: + +# 1 Meditated +# 7 Cleaned the apartment +# 0 Had a headache +# 1 Used habitctl +"; +const PROMPT_OPTIONS: &str = "[y/n/s/⏎] "; + +fn ask_prompt() -> String { + loop { + let prompt_result = rprompt::prompt_reply_stdout(&PROMPT_OPTIONS).unwrap(); + let value = prompt_result.trim_end(); + + match value { + "y" | "n" | "s" | "" => return value.to_string(), + _ => {} + } + } +} fn main() { let matches = app_from_crate!() .template("{bin} {version}\n{author}\n\n{about}\n\nUSAGE:\n {usage}\n\nFLAGS:\n{flags}\n\nSUBCOMMANDS:\n{subcommands}") + .arg(Arg::with_name("habit-dir").long("habit-dir").default_value("~/.habitctl/")) .subcommand( SubCommand::with_name("ask") .about("Ask for status of all habits for a day") @@ -40,13 +58,17 @@ fn main() { .subcommand(SubCommand::with_name("edith").about("Edit list of current habits")) .get_matches(); - let mut habitctl = HabitCtl::new(); + let expanded = shellexpand::tilde(matches.value_of("habit-dir").unwrap()); + let habitctl_dir = Path::new(expanded.as_ref()); + + let mut habitctl = HabitCtl::new(&habitctl_dir); habitctl.load(); let ago: i64 = if habitctl.first_date().is_some() { cmp::min( 7, - Local::today().naive_local() + Local::today() + .naive_local() .signed_duration_since(habitctl.first_date().unwrap()) .num_days(), ) @@ -77,7 +99,7 @@ fn main() { ago }; habitctl.ask(ago); - habitctl.log(&vec![]); + habitctl.log(&[]); } ("edit", Some(_)) => habitctl.edit(), ("edith", Some(_)) => habitctl.edith(), @@ -85,7 +107,7 @@ fn main() { // no subcommand used habitctl.assert_habits(); habitctl.ask(ago); - habitctl.log(&vec![]); + habitctl.log(&[]); } } } @@ -110,42 +132,27 @@ enum DayStatus { } impl HabitCtl { - fn new() -> HabitCtl { - let mut habitctl_dir = dirs::home_dir().unwrap(); - habitctl_dir.push(".habitctl"); + fn new(habitctl_dir: &Path) -> HabitCtl { if !habitctl_dir.is_dir() { println!("Welcome to habitctl!\n"); fs::create_dir(&habitctl_dir).unwrap(); } - let mut habits_file = habitctl_dir.clone(); + let mut habits_file = habitctl_dir.to_owned(); habits_file.push("habits"); if !habits_file.is_file() { - fs::File::create(&habits_file).unwrap(); + fs::write(&habits_file, HABIT_FILE_TEMPLATE).unwrap(); + println!( "Created {}. This file will list your currently tracked habits.", habits_file.to_str().unwrap() ); } - let mut log_file = habitctl_dir.clone(); + let mut log_file = habitctl_dir.to_owned(); log_file.push("log"); if !log_file.is_file() { - fs::File::create(&log_file).unwrap(); - - let file = OpenOptions::new().append(true).open(&habits_file).unwrap(); - write!( - &file, - "# The numbers specifies how often you want to do a habit:\n" - ); - write!( - &file, - "# 1 means daily, 7 means weekly, 0 means you're just tracking the habit. Some examples:\n" - ); - write!( - &file, - "\n# 1 Meditated\n# 7 Cleaned the apartment\n# 0 Had a headache\n# 1 Used habitctl\n" - ); + fs::write(&log_file, "").unwrap(); println!( "Created {}. This file will contain your habit log.\n", @@ -178,20 +185,21 @@ impl HabitCtl { if let Some(last_date) = last_date { if last_date != entry.date { - write!(&file, "\n").unwrap(); + writeln!(&file).unwrap(); } } - write!( + writeln!( &file, - "{}\t{}\t{}\n", + "{}\t{}\t{}", &entry.date.format("%F"), &entry.habit, &entry.value - ).unwrap(); + ) + .unwrap(); } - fn log(&self, filters: &Vec<&str>) { + fn log(&self, filters: &[&str]) { let to = Local::today().naive_local(); let from = to.checked_sub_signed(chrono::Duration::days(100)).unwrap(); @@ -225,9 +233,7 @@ impl HabitCtl { } if !self.habits.is_empty() { - let date = to - .checked_sub_signed(chrono::Duration::days(1)) - .unwrap(); + let date = to.checked_sub_signed(chrono::Duration::days(1)).unwrap(); println!("Yesterday's score: {}%", self.get_score(&date)); } } @@ -275,7 +281,8 @@ impl HabitCtl { } fn ask(&mut self, ago: i64) { - let from = Local::today().naive_local() + let from = Local::today() + .naive_local() .checked_sub_signed(chrono::Duration::days(ago)) .unwrap(); @@ -291,22 +298,12 @@ impl HabitCtl { } for habit in self.get_todo(¤t) { - self.print_habit_row(&habit, log_from, current.clone()); - let l = format!("[y/n/s/⏎] "); + self.print_habit_row(&habit, log_from, current); - let mut value; - loop { - value = String::from(rprompt::prompt_reply_stdout(&l).unwrap()); - value = value.trim_end().to_string(); - - if value == "y" || value == "n" || value == "s" || value == "" { - break; - } - } - - if value != "" { + let value = ask_prompt(); + if !value.is_empty() { self.entry(&Entry { - date: current.clone(), + date: current, habit: habit.name, value, }); @@ -362,15 +359,14 @@ impl HabitCtl { for line in file.lines() { let l = line.unwrap(); - if l == "" { + if l.is_empty() { continue; } let split = l.split('\t'); let parts: Vec<&str> = split.collect(); let entry = Entry { - date: NaiveDate::parse_from_str(parts[0], "%Y-%m-%d") - .unwrap(), + date: NaiveDate::parse_from_str(parts[0], "%Y-%m-%d").unwrap(), habit: parts[1].to_string(), value: parts[2].to_string(), }; @@ -381,7 +377,7 @@ impl HabitCtl { entries } - fn get_entry(&self, date: &NaiveDate, habit: &String) -> Option<&Entry> { + fn get_entry(&self, date: &NaiveDate, habit: &str) -> Option<&Entry> { self.entries .iter() .find(|entry| entry.date == *date && entry.habit == *habit) @@ -400,12 +396,10 @@ impl HabitCtl { } else { DayStatus::NotDone } + } else if self.habit_warning(habit, &date) { + DayStatus::Warning } else { - if self.habit_warning(habit, &date) { - DayStatus::Warning - } else { DayStatus::Unknown - } } } @@ -417,7 +411,7 @@ impl HabitCtl { DayStatus::Satisfied => "─", DayStatus::Skipped => "•", DayStatus::Skipified => "·", - DayStatus::Warning => "!" + DayStatus::Warning => "!", }; String::from(symbol) } @@ -444,7 +438,7 @@ impl HabitCtl { false } - fn habit_skipified(&self, habit: &Habit, date: &NaiveDate) -> bool { + fn habit_skipified(&self, habit: &Habit, date: &NaiveDate) -> bool { if habit.every_days < 1 { return false; } @@ -466,7 +460,7 @@ impl HabitCtl { false } - fn habit_warning(&self, habit: &Habit, date: &NaiveDate) -> bool { + fn habit_warning(&self, habit: &Habit, date: &NaiveDate) -> bool { if habit.every_days < 1 { return false; } @@ -477,9 +471,13 @@ impl HabitCtl { let mut current = *date; while current >= from { if let Some(entry) = self.get_entry(¤t, &habit.name) { - if (entry.value == "y" || entry.value == "s") && current - from > chrono::Duration::days(0) { - return false; - } else if (entry.value == "y" || entry.value == "s") && current - from == chrono::Duration::days(0) { + if (entry.value == "y" || entry.value == "s") + && current - from > chrono::Duration::days(0) + { + return false; + } else if (entry.value == "y" || entry.value == "s") + && current - from == chrono::Duration::days(0) + { return true; } } @@ -506,15 +504,11 @@ impl HabitCtl { } fn first_date(&self) -> Option { - self.get_entries() - .first() - .and_then(|entry| Some(entry.date.clone())) + self.get_entries().first().map(|entry| entry.date) } fn last_date(&self) -> Option { - self.get_entries() - .last() - .and_then(|entry| Some(entry.date.clone())) + self.get_entries().last().map(|entry| entry.date) } fn get_score(&self, score_date: &NaiveDate) -> f32 { @@ -548,7 +542,10 @@ impl HabitCtl { skip.retain(|value| *value); if !todo.is_empty() { - round::ceil((100.0 * done.len() as f32 / (todo.len() - skip.len()) as f32).into(), 1) as f32 + round::ceil( + (100.0 * done.len() as f32 / (todo.len() - skip.len()) as f32).into(), + 1, + ) as f32 } else { 0.0 }