Compare commits

...

10 Commits

7 changed files with 190 additions and 62 deletions

20
Cargo.lock generated
View File

@ -85,6 +85,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"structopt", "structopt",
"time",
"uuid", "uuid",
] ]
@ -106,6 +107,15 @@ version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.16" version = "0.2.16"
@ -271,6 +281,16 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "time"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217"
dependencies = [
"libc",
"num_threads",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.1" version = "1.0.1"

View File

@ -7,10 +7,9 @@ description = "A CLI habit tracker."
readme = "./README.md" readme = "./README.md"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
structopt = "0.3" structopt = "0.3"
uuid = { version = "1.1", features = [ "v4", "fast-rng", "macro-diagnostics" ] } uuid = { version = "1.1", features = [ "v4", "fast-rng", "macro-diagnostics" ] }
serde = { version = "1.0", features = [ "derive" ] } serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1.0" serde_json = "1.0"
time = { version = "0.3", features = [ "local-offset" ] }

7
PRAYER.md Normal file
View File

@ -0,0 +1,7 @@
# Project Prayer
Lord God, you who knows the weakness of Men, and that only by cooperation with
your grace may we overcome the shackles of sin, bless the users of this program
that by a disciplined adherence to the habits they have set forth to form and to
reject, that they may succeed in their enterprise and grow in both faith &
virtue. In the name of Jesus Christ our Lord. Amen.

View File

@ -18,7 +18,8 @@ commands:
- `help` or `h`: show help information - `help` or `h`: show help information
- `list` or `ls`: list the habits available for today - `list` or `ls`: list the habits available for today
- `modify` or `mod`: modify the settings for a habit - `modify` or `mod`: modify the settings for a habit
- `statistics` or `stats`: show statistics on current habits - `stats`: show statistics on current habits
- `vacation` or `vac`: toggle vacation mode
## License ## License

View File

@ -18,6 +18,7 @@
use uuid::Uuid; use uuid::Uuid;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use time::OffsetDateTime;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Habit pub struct Habit
@ -25,28 +26,76 @@ pub struct Habit
uid:String, uid:String,
name:String, name:String,
bad:bool, bad:bool,
weight:u8, priority:char,
// Day 0 is Monday. Use number_days_from_monday() to determine index.
days_active:[bool;7],
done:bool,
streak:i32,
} }
impl Habit impl Habit
{ {
pub fn new(name:String, bad:bool, weight:u8) -> Self pub fn new(name:String, bad:bool, priority:char, days_active:[bool;7]) -> Self
{ {
if ![ 'H','L','M' ].contains(&priority)
{
panic!("Unknown priority {}", priority);
}
Self Self
{ {
uid: Uuid::new_v4().hyphenated().to_string(), uid: Uuid::new_v4().hyphenated().to_string(),
name, name,
bad, bad,
weight, priority,
days_active,
done: false,
streak: 0,
} }
} }
pub fn active_days_string(&self) -> String
{
let mut res:String = String::new();
for (i,v) in self.days_active.iter().enumerate()
{
if *v
{
res.push_str(match i {
0 => "mon",
1 => "tue",
2 => "wed",
3 => "thu",
4 => "fri",
5 => "sat",
6 => "sun",
_ => "unk", // this one will never occur, but the compiler complains
});
res.push(',');
}
}
res.pop();
return res;
}
pub fn active_today(&self) -> bool
{
return self.days_active[OffsetDateTime::now_local().unwrap()
.weekday().number_days_from_monday() as usize]
}
pub fn get_uid(&self) -> &String { &self.uid } pub fn get_uid(&self) -> &String { &self.uid }
pub fn get_name(&self) -> &String { &self.name } pub fn get_name(&self) -> &String { &self.name }
pub fn get_bad(&self) -> bool { self.bad } pub fn get_bad(&self) -> bool { self.bad }
pub fn get_weight(&self) -> u8 { self.weight } pub fn get_priority(&self) -> char { self.priority }
pub fn get_done(&self) -> bool { self.done }
pub fn get_streak(&self) -> i32 { self.streak }
pub fn set_name(&mut self, name:String) { self.name = name; } pub fn set_name(&mut self, name:String) { self.name = name; }
pub fn set_bad(&mut self, bad:bool) { self.bad = bad; } pub fn set_bad(&mut self, bad:bool) { self.bad = bad; }
pub fn set_weight(&mut self, weight:u8) { self.weight = weight; } pub fn set_priority(&mut self, priority:char) { self.priority = priority; }
pub fn set_days(&mut self, days:[bool;7]) { self.days_active = days; }
pub fn set_done(&mut self, done:bool) { self.done = done; }
// TODO: whether set_streak or inc/dec/reset_streak
} }

View File

@ -19,6 +19,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::{BufReader, BufWriter}; use std::io::{BufReader, BufWriter};
use time::Weekday;
use crate::habit::Habit; use crate::habit::Habit;
@ -55,23 +56,27 @@ impl HabitMgr
habits_path.display(), e); habits_path.display(), e);
}); });
} }
Self
{
habits: Vec::new(),
habits_path,
}
}
fn import_habits(&mut self) /*
{ * The list of active habits is necessary for all functions, thus they're
* imported here, allowing member functions which only wish to access
* information to be constant.
*/
let habits_file = OpenOptions::new() let habits_file = OpenOptions::new()
.read(true) .read(true)
.open(&self.habits_path) .open(&habits_path)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
panic!("Error opening file {}:\n{}", self.habits_path.display(), e); panic!("Error opening file {}:\n{}", habits_path.display(), e);
}); });
let habits_reader = BufReader::new(&habits_file); let habits_reader = BufReader::new(&habits_file);
self.habits = serde_json::from_reader(habits_reader).unwrap();
let habits:Vec<Habit> = serde_json::from_reader(habits_reader).unwrap();
Self
{
habits,
habits_path,
}
} }
fn export_habits(&self) fn export_habits(&self)
@ -91,10 +96,40 @@ impl HabitMgr
}); });
} }
pub fn add(&mut self, name:String, bad:bool, weight:u8, _days:String) fn days_array_from_string(days:String) -> [bool;7]
{ {
self.import_habits(); let days_list = days.split(",");
self.habits.push(Habit::new(name.clone(), bad, weight)); let mut days_active:[bool;7] = [ false; 7 ];
for i in days_list
{
match i
{
"mon" =>
days_active[Weekday::Monday.number_days_from_monday() as usize] = true,
"tue" =>
days_active[Weekday::Tuesday.number_days_from_monday() as usize] = true,
"wed" =>
days_active[Weekday::Wednesday.number_days_from_monday() as usize] = true,
"thu" =>
days_active[Weekday::Thursday.number_days_from_monday() as usize] = true,
"fri" =>
days_active[Weekday::Friday.number_days_from_monday() as usize] = true,
"sat" =>
days_active[Weekday::Saturday.number_days_from_monday() as usize] = true,
"sun" =>
days_active[Weekday::Sunday.number_days_from_monday() as usize] = true,
_ =>
panic!("Day {} not recognized!", i),
}
}
return days_active;
}
pub fn add(&mut self, name:String, bad:bool, priority:char, days:String)
{
let days_active = HabitMgr::days_array_from_string(days);
self.habits.push(Habit::new(name.clone(), bad, priority, days_active));
self.export_habits(); self.export_habits();
println!("New habit {} added.", &name); println!("New habit {} added.", &name);
@ -102,30 +137,30 @@ impl HabitMgr
pub fn delete(&mut self, id:usize) pub fn delete(&mut self, id:usize)
{ {
self.import_habits();
let old_habit = self.habits.remove(id); let old_habit = self.habits.remove(id);
self.export_habits(); self.export_habits();
println!("Removed habit {}", old_habit.get_name()); println!("Removed habit {}", old_habit.get_name());
} }
pub fn habit_info(&mut self, id:usize) pub fn habit_info(&self, id:usize)
{ {
self.import_habits();
let habit = &self.habits[id]; let habit = &self.habits[id];
println!("Name: {}", habit.get_name()); println!("Name: {}", habit.get_name());
println!("ID: {}", id); println!("ID: {}", id);
println!("UID: {}", habit.get_uid()); println!("UID: {}", habit.get_uid());
println!("Active Days: {}", habit.active_days_string());
println!("Bad: {}", habit.get_bad()); println!("Bad: {}", habit.get_bad());
println!("Weight: {}", habit.get_weight()); println!("Weight: {}", habit.get_priority());
println!("Done: {}", habit.get_done());
println!("Streak: {}", habit.get_streak());
} }
pub fn modify(&mut self, id:usize, pub fn modify(&mut self, id:usize,
name:Option<String>, name:Option<String>,
toggle_bad:bool, toggle_bad:bool,
weight:Option<u8>, priority:Option<char>,
_days:Option<String>) days:Option<String>)
{ {
self.import_habits();
if name.is_some() if name.is_some()
{ {
self.habits[id].set_name(name.unwrap()); self.habits[id].set_name(name.unwrap());
@ -135,32 +170,45 @@ impl HabitMgr
let is_bad = self.habits[id].get_bad(); let is_bad = self.habits[id].get_bad();
self.habits[id].set_bad(!is_bad); self.habits[id].set_bad(!is_bad);
} }
if weight.is_some() if priority.is_some()
{ {
self.habits[id].set_weight(weight.unwrap()); self.habits[id].set_priority(priority.unwrap());
}
if days.is_some()
{
let days_active = HabitMgr::days_array_from_string(days.unwrap());
self.habits[id].set_days(days_active);
} }
self.export_habits(); self.export_habits();
} }
pub fn list(&mut self, _all:bool, _verbose:bool) pub fn list(&self, all:bool)
{ {
self.import_habits();
if self.habits.is_empty() if self.habits.is_empty()
{ {
println!("There are no habits. Add one!"); println!("There are no habits. Add one!");
} }
else if !all && self.habits.iter().all(|i| !i.active_today())
{
println!("No active habits available today!");
}
else else
{ {
println!(" {0: <3} | {1: <5} | {2: <6} | {3}", println!(" {0: <3} | {1: <5} | {2: <8} | {3: <5} | {4: <6} | {5}",
"id", "bad", "weight", "name"); "id", "bad", "priority", "done", "streak", "name");
for (i, habit) in self.habits.iter().enumerate() for (i, habit) in self.habits.iter().enumerate()
{ {
println!(" {0: <3} | {1: <5} | {2: <6} | {3}", if all || habit.active_today()
{
println!(" {0: <3} | {1: <5} | {2: <8} | {3: <5} | {4: <6} | {5}",
i, i,
habit.get_bad(), habit.get_bad(),
habit.get_weight(), habit.get_priority(),
habit.get_done(),
habit.get_streak(),
habit.get_name()); habit.get_name());
} }
} }
} }
}
} }

View File

@ -42,14 +42,18 @@ enum Command
#[structopt(alias = "a")] #[structopt(alias = "a")]
Add Add
{ {
#[structopt(help = "name of the new habit")] #[structopt(help = "Name of the new habit")]
name:String, name:String,
#[structopt(long, short, default_value = "mon,tue,wed,thu,fri,sat,sun")] #[structopt(long, short, help = "Days the new habit is active",
default_value = "mon,tue,wed,thu,fri,sat,sun")]
days:String, days:String,
#[structopt(long)] #[structopt(long, help = "Assign habit as a bad habit (negative points)")]
bad:bool, bad:bool,
#[structopt(short, long, default_value = "5")] #[structopt(short,
weight:u8, long,
help = "Priority of the new habit (L, M, or H)",
default_value = "M")]
priority:char,
}, },
Commit { }, Commit { },
#[structopt(alias = "del")] #[structopt(alias = "del")]
@ -59,31 +63,31 @@ enum Command
}, },
#[structopt(alias = "i")] #[structopt(alias = "i")]
Info { Info {
#[structopt(help = "ID of the habit to show information for")]
id:usize, id:usize,
}, },
#[structopt(alias = "ls")] #[structopt(alias = "ls")]
List List
{ {
#[structopt(short, long, help = "list all active habits")] #[structopt(short, long, help = "List all active habits")]
all:bool, all:bool,
#[structopt(short, long, help = "show UUIDs")]
verbose:bool,
}, },
#[structopt(alias = "mod")] #[structopt(alias = "mod")]
Modify { Modify {
#[structopt(help = "ID of the habit to modify")] #[structopt(help = "ID of the habit to modify")]
id:usize, id:usize,
#[structopt(short, long)] #[structopt(short, long, help = "New name for the habit")]
name:Option<String>, name:Option<String>,
#[structopt(short, long)] #[structopt(short, long, help = "New priority for the habit (L, M, or H)")]
weight:Option<u8>, priority:Option<char>,
#[structopt(long, short)] #[structopt(long, short, help = "New days the habit is active")]
days:Option<String>, days:Option<String>,
#[structopt(long)] #[structopt(long, help = "Toggle the 'bad' value of the habit")]
toggle_bad:bool, toggle_bad:bool,
}, },
#[structopt(alias = "stats")] Stats { },
Statistics { }, #[structopt(alias = "vac")]
Vacation { },
} }
fn main() fn main()
@ -112,20 +116,20 @@ fn main()
match opts.cmd match opts.cmd
{ {
None => hmgr.list(false, false), None => hmgr.list(false),
Some(c) => Some(c) =>
match c match c
{ {
Command::Add { name, days, bad, weight } => Command::Add { name, days, bad, priority } =>
hmgr.add(name, bad, weight, days), hmgr.add(name, bad, priority, days),
Command::Delete { id } => Command::Delete { id } =>
hmgr.delete(id), hmgr.delete(id),
Command::Info { id } => Command::Info { id } =>
hmgr.habit_info(id), hmgr.habit_info(id),
Command::List { all, verbose } => Command::List { all } =>
hmgr.list(all, verbose), hmgr.list(all),
Command::Modify { id, name, weight, days, toggle_bad } => Command::Modify { id, name, priority, days, toggle_bad } =>
hmgr.modify(id, name, toggle_bad, weight, days), hmgr.modify(id, name, toggle_bad, priority, days),
_ => todo!(), _ => todo!(),
}, },
} }