aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/dependabot.yml8
-rw-r--r--notes.txt19
-rw-r--r--src/app.rs58
-rw-r--r--src/habit.rs271
-rw-r--r--src/habit/bit.rs117
-rw-r--r--src/habit/count.rs102
-rw-r--r--src/habit/mod.rs20
-rw-r--r--src/habit/prelude.rs20
-rw-r--r--src/habit/traits.rs94
-rw-r--r--src/main.rs2
-rw-r--r--src/views.rs77
11 files changed, 472 insertions, 316 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..f3b0f62
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
1version: 2
2updates:
3- package-ecosystem: cargo
4 directory: "/"
5 schedule:
6 interval: daily
7 open-pull-requests-limit: 10
8 target-branch: master
diff --git a/notes.txt b/notes.txt
index 675b78f..09c3098 100644
--- a/notes.txt
+++ b/notes.txt
@@ -29,10 +29,14 @@ Modes:
29 29
30Command mode: 30Command mode:
31 - add command 31 - add command
32 * add <name> <type> <goal>
33 * add <name> --type <type> [--goal <goal>]
34 * interactive add command via questionnaire?
32 - edit command? 35 - edit command?
33 * edit <name> <new-type> <new-goal> 36 * edit <name> <new-type> <new-goal>
34 * edit <name> --goal <new-goal> 37 * edit <name> --goal <new-goal>
35 * edit <name> --type <new-type> 38 * edit <name> --type <new-type>
39 * interactive edit command via questionnaire?
36 - delete command 40 - delete command
37 * delete <name> 41 * delete <name>
38 * delete _ (deletes focused?) 42 * delete _ (deletes focused?)
@@ -40,7 +44,22 @@ Command mode:
40 * month-prev mprev 44 * month-prev mprev
41 * month-next mnext 45 * month-next mnext
42 46
47Interface:
48 - move view port if focused view goes outside bounds
49 - tab completion for command mode? requires lex table
50 - move command window to bottom, styling
51 - prefix command window with `:`
52
43Undo-tree: 53Undo-tree:
44 - store app states in memory 54 - store app states in memory
45 - should store diffs? or entire state? 55 - should store diffs? or entire state?
46 - ideal undo depth limit? 56 - ideal undo depth limit?
57
58Auto-trackable habits
59 - allow editing these habits via cli
60 - can track commits, crons
61 - disallow editing these habits via curses
62 - storage
63 * will be mutex with non-auto habits
64 * serialize and save separately each other? [imp]
65
diff --git a/src/app.rs b/src/app.rs
index 82096e1..f8797fc 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -9,26 +9,13 @@ use cursive::{Printer, Vec2};
9 9
10use chrono::{Local, NaiveDate}; 10use chrono::{Local, NaiveDate};
11 11
12use crate::habit::{Bit, Count, Habit, HabitWrapper}; 12use crate::habit::{Bit, Count, Habit, HabitWrapper, ViewMode};
13use crate::utils; 13use crate::utils;
14use crate::Command; 14use crate::Command;
15use crate::CONFIGURATION; 15use crate::CONFIGURATION;
16 16
17use serde::{Deserialize, Serialize}; 17use serde::{Deserialize, Serialize};
18 18
19#[derive(PartialEq, Serialize, Deserialize)]
20pub enum ViewMode {
21 Day,
22 Month,
23 Year,
24}
25
26impl std::default::Default for ViewMode {
27 fn default() -> Self {
28 ViewMode::Month
29 }
30}
31
32struct StatusLine(String, String); 19struct StatusLine(String, String);
33 20
34#[derive(Serialize, Deserialize)] 21#[derive(Serialize, Deserialize)]
@@ -41,9 +28,6 @@ pub struct App {
41 focus: usize, 28 focus: usize,
42 29
43 #[serde(skip)] 30 #[serde(skip)]
44 view_mode: ViewMode,
45
46 #[serde(skip)]
47 view_month_offset: u32, 31 view_month_offset: u32,
48} 32}
49 33
@@ -51,7 +35,6 @@ impl App {
51 pub fn new() -> Self { 35 pub fn new() -> Self {
52 return App { 36 return App {
53 habits: vec![], 37 habits: vec![],
54 view_mode: ViewMode::Day,
55 focus: 0, 38 focus: 0,
56 view_month_offset: 0, 39 view_month_offset: 0,
57 }; 40 };
@@ -65,9 +48,9 @@ impl App {
65 self.habits.retain(|h| h.get_name() != name); 48 self.habits.retain(|h| h.get_name() != name);
66 } 49 }
67 50
68 pub fn set_mode(&mut self, set_mode: ViewMode) { 51 pub fn set_mode(&mut self, mode: ViewMode) {
69 if set_mode != self.view_mode { 52 if !self.habits.is_empty() {
70 self.view_mode = set_mode; 53 self.habits[self.focus].set_view_mode(mode);
71 } 54 }
72 } 55 }
73 56
@@ -128,7 +111,7 @@ impl App {
128 fn status(&self) -> StatusLine { 111 fn status(&self) -> StatusLine {
129 let today = chrono::Local::now().naive_utc().date(); 112 let today = chrono::Local::now().naive_utc().date();
130 let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::<u32>(); 113 let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::<u32>();
131 let total = self.habits.iter().map(|h| h.total()).sum::<u32>(); 114 let total = self.habits.iter().map(|h| h.goal()).sum::<u32>();
132 let completed = total - remaining; 115 let completed = total - remaining;
133 116
134 let timestamp = if self.view_month_offset == 0 { 117 let timestamp = if self.view_month_offset == 0 {
@@ -141,7 +124,7 @@ impl App {
141 let months = self.view_month_offset; 124 let months = self.view_month_offset;
142 format!( 125 format!(
143 "{:>width$}", 126 "{:>width$}",
144 format!("{} months ago", self.view_month_offset), 127 format!("{} months ago", months),
145 width = CONFIGURATION.view_width * CONFIGURATION.grid_width 128 width = CONFIGURATION.view_width * CONFIGURATION.grid_width
146 ) 129 )
147 }; 130 };
@@ -233,8 +216,8 @@ impl View for App {
233 } 216 }
234 217
235 offset = offset.map_x(|_| 0).map_y(|_| self.max_size().y - 2); 218 offset = offset.map_x(|_| 0).map_y(|_| self.max_size().y - 2);
236 printer.print(offset, &self.status().1); // right 219 printer.print(offset, &self.status().1); // right status
237 printer.print(offset, &self.status().0); // left 220 printer.print(offset, &self.status().0); // left status
238 } 221 }
239 222
240 fn required_size(&mut self, _: Vec2) -> Vec2 { 223 fn required_size(&mut self, _: Vec2) -> Vec2 {
@@ -243,7 +226,7 @@ impl View for App {
243 let view_height = CONFIGURATION.view_height; 226 let view_height = CONFIGURATION.view_height;
244 let width = { 227 let width = {
245 if self.habits.len() > 0 { 228 if self.habits.len() > 0 {
246 grid_width * view_width 229 grid_width * (view_width + 2)
247 } else { 230 } else {
248 0 231 0
249 } 232 }
@@ -300,6 +283,29 @@ impl View for App {
300 self.save_state(); 283 self.save_state();
301 return EventResult::with_cb(|s| s.quit()); 284 return EventResult::with_cb(|s| s.quit());
302 } 285 }
286 Event::Char('v') => {
287 if self.habits.is_empty() {
288 return EventResult::Consumed(None);
289 }
290 if self.habits[self.focus].view_mode() == ViewMode::Week {
291 self.set_mode(ViewMode::Day)
292 } else {
293 self.set_mode(ViewMode::Week)
294 }
295 return EventResult::Consumed(None);
296 }
297 Event::Char('V') => {
298 for habit in self.habits.iter_mut() {
299 habit.set_view_mode(ViewMode::Week);
300 }
301 return EventResult::Consumed(None);
302 }
303 Event::Key(Key::Esc) => {
304 for habit in self.habits.iter_mut() {
305 habit.set_view_mode(ViewMode::Day);
306 }
307 return EventResult::Consumed(None);
308 }
303 309
304 /* We want sifting to be an app level function, 310 /* We want sifting to be an app level function,
305 * that later trickles down into each habit 311 * that later trickles down into each habit
diff --git a/src/habit.rs b/src/habit.rs
deleted file mode 100644
index 48dd363..0000000
--- a/src/habit.rs
+++ /dev/null
@@ -1,271 +0,0 @@
1use std::collections::HashMap;
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6use cursive::direction::Direction;
7use cursive::event::{Event, EventResult};
8use cursive::{Printer, Vec2};
9
10use crate::views::ShadowView;
11use crate::CONFIGURATION;
12
13pub enum TrackEvent {
14 Increment,
15 Decrement,
16}
17
18#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
19pub struct CustomBool(bool);
20
21use std::fmt;
22impl fmt::Display for CustomBool {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 write!(
25 f,
26 "{:^3}",
27 if self.0 {
28 CONFIGURATION.true_chr
29 } else {
30 CONFIGURATION.false_chr
31 }
32 )
33 }
34}
35
36impl From<bool> for CustomBool {
37 fn from(b: bool) -> Self {
38 CustomBool(b)
39 }
40}
41
42pub trait Habit {
43 type HabitType;
44
45 fn set_name(&mut self, name: impl AsRef<str>);
46 fn set_goal(&mut self, goal: Self::HabitType);
47 fn name(&self) -> String;
48 fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType>;
49 fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType);
50 fn reached_goal(&self, date: NaiveDate) -> bool;
51 fn remaining(&self, date: NaiveDate) -> u32;
52 fn total(&self) -> u32;
53 fn modify(&mut self, date: NaiveDate, event: TrackEvent);
54 fn set_view_month_offset(&mut self, offset: u32);
55 fn view_month_offset(&self) -> u32;
56}
57
58#[typetag::serde(tag = "type")]
59pub trait HabitWrapper: erased_serde::Serialize {
60 fn remaining(&self, date: NaiveDate) -> u32;
61 fn total(&self) -> u32;
62 fn modify(&mut self, date: NaiveDate, event: TrackEvent);
63 fn draw(&self, printer: &Printer);
64 fn on_event(&mut self, event: Event) -> EventResult;
65 fn required_size(&mut self, _: Vec2) -> Vec2;
66 fn take_focus(&mut self, _: Direction) -> bool;
67 fn set_view_month_offset(&mut self, offset: u32);
68 fn view_month_offset(&self) -> u32;
69 fn get_name(&self) -> String;
70}
71
72macro_rules! auto_habit_impl {
73 ($struct_name:ident) => {
74 #[typetag::serde]
75 impl HabitWrapper for $struct_name {
76 fn remaining(&self, date: NaiveDate) -> u32 {
77 Habit::remaining(self, date)
78 }
79 fn total(&self) -> u32 {
80 Habit::total(self)
81 }
82 fn modify(&mut self, date: NaiveDate, event: TrackEvent) {
83 Habit::modify(self, date, event);
84 }
85 fn draw(&self, printer: &Printer) {
86 ShadowView::draw(self, printer)
87 }
88 fn on_event(&mut self, event: Event) -> EventResult {
89 ShadowView::on_event(self, event)
90 }
91 fn required_size(&mut self, x: Vec2) -> Vec2 {
92 ShadowView::required_size(self, x)
93 }
94 fn take_focus(&mut self, d: Direction) -> bool {
95 ShadowView::take_focus(self, d)
96 }
97 fn set_view_month_offset(&mut self, offset: u32) {
98 Habit::set_view_month_offset(self, offset)
99 }
100 fn view_month_offset(&self) -> u32 {
101 Habit::view_month_offset(self)
102 }
103 fn get_name(&self) -> String {
104 Habit::name(self)
105 }
106 }
107 };
108}
109
110auto_habit_impl!(Count);
111auto_habit_impl!(Bit);
112
113#[derive(Debug, Serialize, Deserialize)]
114pub struct Count {
115 name: String,
116 stats: HashMap<NaiveDate, u32>,
117 goal: u32,
118
119 #[serde(skip)]
120 view_month_offset: u32,
121}
122
123impl Count {
124 pub fn new(name: impl AsRef<str>, goal: u32) -> Self {
125 return Count {
126 name: name.as_ref().to_owned(),
127 stats: HashMap::new(),
128 goal,
129 view_month_offset: 0,
130 };
131 }
132}
133
134impl Habit for Count {
135 type HabitType = u32;
136
137 fn name(&self) -> String {
138 return self.name.clone();
139 }
140 fn set_name(&mut self, n: impl AsRef<str>) {
141 self.name = n.as_ref().to_owned();
142 }
143 fn set_goal(&mut self, g: Self::HabitType) {
144 self.goal = g;
145 }
146 fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType> {
147 self.stats.get(&date)
148 }
149 fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType) {
150 *self.stats.entry(date).or_insert(val) = val;
151 }
152 fn reached_goal(&self, date: NaiveDate) -> bool {
153 if let Some(val) = self.stats.get(&date) {
154 if val >= &self.goal {
155 return true;
156 }
157 }
158 return false;
159 }
160 fn remaining(&self, date: NaiveDate) -> u32 {
161 if self.reached_goal(date) {
162 return 0;
163 } else {
164 if let Some(val) = self.stats.get(&date) {
165 return self.goal - val;
166 } else {
167 return self.goal;
168 }
169 }
170 }
171 fn total(&self) -> u32 {
172 return self.goal;
173 }
174 fn modify(&mut self, date: NaiveDate, event: TrackEvent) {
175 if let Some(val) = self.stats.get_mut(&date) {
176 match event {
177 TrackEvent::Increment => *val += 1,
178 TrackEvent::Decrement => {
179 if *val > 0 {
180 *val -= 1
181 } else {
182 *val = 0
183 };
184 }
185 }
186 } else {
187 self.insert_entry(date, 1);
188 }
189 }
190 fn set_view_month_offset(&mut self, offset: u32) {
191 self.view_month_offset = offset;
192 }
193 fn view_month_offset(&self) -> u32 {
194 self.view_month_offset
195 }
196}
197
198#[derive(Debug, Serialize, Deserialize)]
199pub struct Bit {
200 name: String,
201 stats: HashMap<NaiveDate, CustomBool>,
202 goal: CustomBool,
203
204 #[serde(skip)]
205 view_month_offset: u32,
206}
207
208impl Bit {
209 pub fn new(name: impl AsRef<str>) -> Self {
210 return Bit {
211 name: name.as_ref().to_owned(),
212 stats: HashMap::new(),
213 goal: CustomBool(true),
214 view_month_offset: 0,
215 };
216 }
217}
218
219impl Habit for Bit {
220 type HabitType = CustomBool;
221 fn name(&self) -> String {
222 return self.name.clone();
223 }
224 fn set_name(&mut self, n: impl AsRef<str>) {
225 self.name = n.as_ref().to_owned();
226 }
227 fn set_goal(&mut self, g: Self::HabitType) {
228 self.goal = g;
229 }
230 fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType> {
231 self.stats.get(&date)
232 }
233 fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType) {
234 *self.stats.entry(date).or_insert(val) = val;
235 }
236 fn reached_goal(&self, date: NaiveDate) -> bool {
237 if let Some(val) = self.stats.get(&date) {
238 if val.0 >= self.goal.0 {
239 return true;
240 }
241 }
242 return false;
243 }
244 fn remaining(&self, date: NaiveDate) -> u32 {
245 if let Some(val) = self.stats.get(&date) {
246 if val.0 {
247 return 0;
248 } else {
249 return 1;
250 }
251 } else {
252 return 1;
253 }
254 }
255 fn total(&self) -> u32 {
256 return 1;
257 }
258 fn modify(&mut self, date: NaiveDate, _: TrackEvent) {
259 if let Some(val) = self.stats.get_mut(&date) {
260 *val = (val.0 ^ true).into();
261 } else {
262 self.insert_entry(date, CustomBool(true));
263 }
264 }
265 fn set_view_month_offset(&mut self, offset: u32) {
266 self.view_month_offset = offset;
267 }
268 fn view_month_offset(&self) -> u32 {
269 self.view_month_offset
270 }
271}
diff --git a/src/habit/bit.rs b/src/habit/bit.rs
new file mode 100644
index 0000000..292b96a
--- /dev/null
+++ b/src/habit/bit.rs
@@ -0,0 +1,117 @@
1use std::collections::HashMap;
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6use crate::habit::traits::Habit;
7use crate::habit::{TrackEvent, ViewMode};
8use crate::CONFIGURATION;
9
10#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
11pub struct CustomBool(bool);
12
13use std::fmt;
14impl fmt::Display for CustomBool {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 write!(
17 f,
18 "{:^3}",
19 if self.0 {
20 CONFIGURATION.true_chr
21 } else {
22 CONFIGURATION.false_chr
23 }
24 )
25 }
26}
27
28impl From<bool> for CustomBool {
29 fn from(b: bool) -> Self {
30 CustomBool(b)
31 }
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct Bit {
36 name: String,
37 stats: HashMap<NaiveDate, CustomBool>,
38 goal: CustomBool,
39
40 #[serde(skip)]
41 view_month_offset: u32,
42
43 #[serde(skip)]
44 view_mode: ViewMode,
45}
46
47impl Bit {
48 pub fn new(name: impl AsRef<str>) -> Self {
49 return Bit {
50 name: name.as_ref().to_owned(),
51 stats: HashMap::new(),
52 goal: CustomBool(true),
53 view_month_offset: 0,
54 view_mode: ViewMode::Day,
55 };
56 }
57}
58
59impl Habit for Bit {
60 type HabitType = CustomBool;
61 fn name(&self) -> String {
62 return self.name.clone();
63 }
64 fn set_name(&mut self, n: impl AsRef<str>) {
65 self.name = n.as_ref().to_owned();
66 }
67 fn set_goal(&mut self, g: Self::HabitType) {
68 self.goal = g;
69 }
70 fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType> {
71 self.stats.get(&date)
72 }
73 fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType) {
74 *self.stats.entry(date).or_insert(val) = val;
75 }
76 fn reached_goal(&self, date: NaiveDate) -> bool {
77 if let Some(val) = self.stats.get(&date) {
78 if val.0 >= self.goal.0 {
79 return true;
80 }
81 }
82 return false;
83 }
84 fn remaining(&self, date: NaiveDate) -> u32 {
85 if let Some(val) = self.stats.get(&date) {
86 if val.0 {
87 return 0;
88 } else {
89 return 1;
90 }
91 } else {
92 return 1;
93 }
94 }
95 fn goal(&self) -> u32 {
96 return 1;
97 }
98 fn modify(&mut self, date: NaiveDate, _: TrackEvent) {
99 if let Some(val) = self.stats.get_mut(&date) {
100 *val = (val.0 ^ true).into();
101 } else {
102 self.insert_entry(date, CustomBool(true));
103 }
104 }
105 fn set_view_month_offset(&mut self, offset: u32) {
106 self.view_month_offset = offset;
107 }
108 fn view_month_offset(&self) -> u32 {
109 self.view_month_offset
110 }
111 fn set_view_mode(&mut self, mode: ViewMode) {
112 self.view_mode = mode;
113 }
114 fn view_mode(&self) -> ViewMode {
115 self.view_mode
116 }
117}
diff --git a/src/habit/count.rs b/src/habit/count.rs
new file mode 100644
index 0000000..a0e0aee
--- /dev/null
+++ b/src/habit/count.rs
@@ -0,0 +1,102 @@
1use std::collections::HashMap;
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6use crate::habit::traits::Habit;
7use crate::habit::{TrackEvent, ViewMode};
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct Count {
11 name: String,
12 stats: HashMap<NaiveDate, u32>,
13 goal: u32,
14
15 #[serde(skip)]
16 view_month_offset: u32,
17
18 #[serde(skip)]
19 view_mode: ViewMode,
20}
21
22impl Count {
23 pub fn new(name: impl AsRef<str>, goal: u32) -> Self {
24 return Count {
25 name: name.as_ref().to_owned(),
26 stats: HashMap::new(),
27 goal,
28 view_month_offset: 0,
29 view_mode: ViewMode::Day,
30 };
31 }
32}
33
34impl Habit for Count {
35 type HabitType = u32;
36
37 fn name(&self) -> String {
38 return self.name.clone();
39 }
40 fn set_name(&mut self, n: impl AsRef<str>) {
41 self.name = n.as_ref().to_owned();
42 }
43 fn set_goal(&mut self, g: Self::HabitType) {
44 self.goal = g;
45 }
46 fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType> {
47 self.stats.get(&date)
48 }
49 fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType) {
50 *self.stats.entry(date).or_insert(val) = val;
51 }
52 fn reached_goal(&self, date: NaiveDate) -> bool {
53 if let Some(val) = self.stats.get(&date) {
54 if val >= &self.goal {
55 return true;
56 }
57 }
58 return false;
59 }
60 fn remaining(&self, date: NaiveDate) -> u32 {
61 if self.reached_goal(date) {
62 return 0;
63 } else {
64 if let Some(val) = self.stats.get(&date) {
65 return self.goal - val;
66 } else {
67 return self.goal;
68 }
69 }
70 }
71 fn goal(&self) -> u32 {
72 return self.goal;
73 }
74 fn modify(&mut self, date: NaiveDate, event: TrackEvent) {
75 if let Some(val) = self.stats.get_mut(&date) {
76 match event {
77 TrackEvent::Increment => *val += 1,
78 TrackEvent::Decrement => {
79 if *val > 0 {
80 *val -= 1
81 } else {
82 *val = 0
83 };
84 }
85 }
86 } else {
87 self.insert_entry(date, 1);
88 }
89 }
90 fn set_view_month_offset(&mut self, offset: u32) {
91 self.view_month_offset = offset;
92 }
93 fn view_month_offset(&self) -> u32 {
94 self.view_month_offset
95 }
96 fn set_view_mode(&mut self, mode: ViewMode) {
97 self.view_mode = mode;
98 }
99 fn view_mode(&self) -> ViewMode {
100 self.view_mode
101 }
102}
diff --git a/src/habit/mod.rs b/src/habit/mod.rs
new file mode 100644
index 0000000..482ca06
--- /dev/null
+++ b/src/habit/mod.rs
@@ -0,0 +1,20 @@
1use std::collections::HashMap;
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6use cursive::direction::Direction;
7use cursive::event::{Event, EventResult};
8use cursive::{Printer, Vec2};
9
10mod traits;
11pub use traits::{Habit, HabitWrapper};
12
13mod count;
14pub use count::Count;
15
16mod bit;
17pub use bit::Bit;
18
19mod prelude;
20pub use prelude::{TrackEvent, ViewMode};
diff --git a/src/habit/prelude.rs b/src/habit/prelude.rs
new file mode 100644
index 0000000..9196f00
--- /dev/null
+++ b/src/habit/prelude.rs
@@ -0,0 +1,20 @@
1use serde::{Deserialize, Serialize};
2
3pub enum TrackEvent {
4 Increment,
5 Decrement,
6}
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
9pub enum ViewMode {
10 Day,
11 Week,
12 Month,
13 Year,
14}
15
16impl std::default::Default for ViewMode {
17 fn default() -> Self {
18 ViewMode::Day
19 }
20}
diff --git a/src/habit/traits.rs b/src/habit/traits.rs
new file mode 100644
index 0000000..e28e55d
--- /dev/null
+++ b/src/habit/traits.rs
@@ -0,0 +1,94 @@
1use chrono::NaiveDate;
2use cursive::direction::Direction;
3use cursive::event::{Event, EventResult};
4use cursive::{Printer, Vec2};
5
6use typetag;
7
8use crate::habit::{Bit, Count, TrackEvent, ViewMode};
9use crate::views::ShadowView;
10
11pub trait Habit {
12 type HabitType;
13
14 fn set_name(&mut self, name: impl AsRef<str>);
15 fn set_goal(&mut self, goal: Self::HabitType);
16 fn name(&self) -> String;
17 fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType>;
18 fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType);
19 fn reached_goal(&self, date: NaiveDate) -> bool;
20 fn remaining(&self, date: NaiveDate) -> u32;
21 fn goal(&self) -> u32;
22 fn modify(&mut self, date: NaiveDate, event: TrackEvent);
23
24 fn set_view_month_offset(&mut self, offset: u32);
25 fn view_month_offset(&self) -> u32;
26
27 fn set_view_mode(&mut self, mode: ViewMode);
28 fn view_mode(&self) -> ViewMode;
29}
30
31#[typetag::serde(tag = "type")]
32pub trait HabitWrapper: erased_serde::Serialize {
33 fn remaining(&self, date: NaiveDate) -> u32;
34 fn goal(&self) -> u32;
35 fn modify(&mut self, date: NaiveDate, event: TrackEvent);
36 fn draw(&self, printer: &Printer);
37 fn on_event(&mut self, event: Event) -> EventResult;
38 fn required_size(&mut self, _: Vec2) -> Vec2;
39 fn take_focus(&mut self, _: Direction) -> bool;
40 fn get_name(&self) -> String;
41
42 fn set_view_month_offset(&mut self, offset: u32);
43 fn view_month_offset(&self) -> u32;
44
45 fn set_view_mode(&mut self, mode: ViewMode);
46 fn view_mode(&self) -> ViewMode;
47}
48
49macro_rules! auto_habit_impl {
50 ($struct_name:ident) => {
51 #[typetag::serde]
52 impl HabitWrapper for $struct_name {
53 fn remaining(&self, date: NaiveDate) -> u32 {
54 Habit::remaining(self, date)
55 }
56 fn goal(&self) -> u32 {
57 Habit::goal(self)
58 }
59 fn modify(&mut self, date: NaiveDate, event: TrackEvent) {
60 Habit::modify(self, date, event);
61 }
62 fn draw(&self, printer: &Printer) {
63 ShadowView::draw(self, printer)
64 }
65 fn on_event(&mut self, event: Event) -> EventResult {
66 ShadowView::on_event(self, event)
67 }
68 fn required_size(&mut self, x: Vec2) -> Vec2 {
69 ShadowView::required_size(self, x)
70 }
71 fn take_focus(&mut self, d: Direction) -> bool {
72 ShadowView::take_focus(self, d)
73 }
74 fn get_name(&self) -> String {
75 Habit::name(self)
76 }
77 fn set_view_month_offset(&mut self, offset: u32) {
78 Habit::set_view_month_offset(self, offset)
79 }
80 fn view_month_offset(&self) -> u32 {
81 Habit::view_month_offset(self)
82 }
83 fn set_view_mode(&mut self, mode: ViewMode) {
84 Habit::set_view_mode(self, mode)
85 }
86 fn view_mode(&self) -> ViewMode {
87 Habit::view_mode(self)
88 }
89 }
90 };
91}
92
93auto_habit_impl!(Count);
94auto_habit_impl!(Bit);
diff --git a/src/main.rs b/src/main.rs
index 4f91990..387dc64 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,7 @@ mod theme;
9mod utils; 9mod utils;
10mod views; 10mod views;
11 11
12use crate::app::{App, ViewMode}; 12use crate::app::App;
13use crate::command::{open_command_window, Command}; 13use crate::command::{open_command_window, Command};
14use crate::habit::{Bit, Count, Habit}; 14use crate::habit::{Bit, Count, Habit};
15use crate::utils::{load_configuration_file, AppConfig}; 15use crate::utils::{load_configuration_file, AppConfig};
diff --git a/src/views.rs b/src/views.rs
index d25e59b..9e4a844 100644
--- a/src/views.rs
+++ b/src/views.rs
@@ -7,7 +7,8 @@ use cursive::{Printer, Vec2};
7use chrono::prelude::*; 7use chrono::prelude::*;
8use chrono::{Duration, Local, NaiveDate}; 8use chrono::{Duration, Local, NaiveDate};
9 9
10use crate::habit::{Bit, Count, Habit, TrackEvent}; 10use crate::habit::{Bit, Count, Habit, TrackEvent, ViewMode};
11
11use crate::CONFIGURATION; 12use crate::CONFIGURATION;
12 13
13pub trait ShadowView { 14pub trait ShadowView {
@@ -62,26 +63,66 @@ where
62 }, 63 },
63 ); 64 );
64 65
65 let mut i = 1; 66 let draw_month = |printer: &Printer| {
66 while let Some(d) = NaiveDate::from_ymd_opt(year, month, i) { 67 let days = (1..31)
67 let day_style; 68 .map(|i| NaiveDate::from_ymd_opt(year, month, i))
68 if self.reached_goal(d) { 69 .flatten() // dates 28-31 may not exist, ignore them if they don't
69 day_style = goal_reached_style; 70 .collect::<Vec<_>>();
70 } else { 71 for (week, line_nr) in days.chunks(7).zip(2..) {
71 day_style = todo_style; 72 let weekly_goal = self.goal() * week.len() as u32;
72 } 73 let is_this_week = week.contains(&Local::now().naive_utc().date());
73 let coords: Vec2 = ((i % 7) * 3, i / 7 + 2).into(); 74 let remaining = week.iter().map(|&i| self.remaining(i)).sum::<u32>();
74 if let Some(c) = self.get_by_date(d) { 75 let completions = weekly_goal - remaining;
75 printer.with_style(day_style, |p| { 76 let full = CONFIGURATION.view_width - 8;
76 p.print(coords, &format!("{:^3}", c)); 77 let bars_to_fill = (completions * full as u32) / weekly_goal;
77 }); 78 let percentage = (completions as f64 * 100.) / weekly_goal as f64;
78 } else {
79 printer.with_style(future_style, |p| { 79 printer.with_style(future_style, |p| {
80 p.print(coords, &format!("{:^3}", CONFIGURATION.future_chr)); 80 p.print((4, line_nr), &"―".repeat(full));
81 });
82 printer.with_style(goal_reached_style, |p| {
83 p.print((4, line_nr), &"―".repeat(bars_to_fill as usize));
81 }); 84 });
85 printer.with_style(
86 if is_this_week {
87 Style::none()
88 } else {
89 future_style
90 },
91 |p| {
92 p.print((0, line_nr), &format!("{:2.0}% ", percentage));
93 },
94 );
82 } 95 }
83 i += 1; 96 };
84 } 97
98 let draw_day = |printer: &Printer| {
99 let mut i = 0;
100 while let Some(d) = NaiveDate::from_ymd_opt(year, month, i + 1) {
101 let day_style;
102 if self.reached_goal(d) {
103 day_style = goal_reached_style;
104 } else {
105 day_style = todo_style;
106 }
107 let coords: Vec2 = ((i % 7) * 3, i / 7 + 2).into();
108 if let Some(c) = self.get_by_date(d) {
109 printer.with_style(day_style, |p| {
110 p.print(coords, &format!("{:^3}", c));
111 });
112 } else {
113 printer.with_style(future_style, |p| {
114 p.print(coords, &format!("{:^3}", CONFIGURATION.future_chr));
115 });
116 }
117 i += 1;
118 }
119 };
120
121 match self.view_mode() {
122 ViewMode::Day => draw_day(printer),
123 ViewMode::Week => draw_month(printer),
124 _ => draw_day(printer),
125 };
85 } 126 }
86 127
87 fn required_size(&mut self, _: Vec2) -> Vec2 { 128 fn required_size(&mut self, _: Vec2) -> Vec2 {