diff options
-rw-r--r-- | .github/dependabot.yml | 8 | ||||
-rw-r--r-- | notes.txt | 19 | ||||
-rw-r--r-- | src/app.rs | 58 | ||||
-rw-r--r-- | src/habit.rs | 271 | ||||
-rw-r--r-- | src/habit/bit.rs | 117 | ||||
-rw-r--r-- | src/habit/count.rs | 102 | ||||
-rw-r--r-- | src/habit/mod.rs | 20 | ||||
-rw-r--r-- | src/habit/prelude.rs | 20 | ||||
-rw-r--r-- | src/habit/traits.rs | 94 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/views.rs | 77 |
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 @@ | |||
1 | version: 2 | ||
2 | updates: | ||
3 | - package-ecosystem: cargo | ||
4 | directory: "/" | ||
5 | schedule: | ||
6 | interval: daily | ||
7 | open-pull-requests-limit: 10 | ||
8 | target-branch: master | ||
@@ -29,10 +29,14 @@ Modes: | |||
29 | 29 | ||
30 | Command mode: | 30 | Command 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 | ||
47 | Interface: | ||
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 | |||
43 | Undo-tree: | 53 | Undo-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 | |||
58 | Auto-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 | |||
@@ -9,26 +9,13 @@ use cursive::{Printer, Vec2}; | |||
9 | 9 | ||
10 | use chrono::{Local, NaiveDate}; | 10 | use chrono::{Local, NaiveDate}; |
11 | 11 | ||
12 | use crate::habit::{Bit, Count, Habit, HabitWrapper}; | 12 | use crate::habit::{Bit, Count, Habit, HabitWrapper, ViewMode}; |
13 | use crate::utils; | 13 | use crate::utils; |
14 | use crate::Command; | 14 | use crate::Command; |
15 | use crate::CONFIGURATION; | 15 | use crate::CONFIGURATION; |
16 | 16 | ||
17 | use serde::{Deserialize, Serialize}; | 17 | use serde::{Deserialize, Serialize}; |
18 | 18 | ||
19 | #[derive(PartialEq, Serialize, Deserialize)] | ||
20 | pub enum ViewMode { | ||
21 | Day, | ||
22 | Month, | ||
23 | Year, | ||
24 | } | ||
25 | |||
26 | impl std::default::Default for ViewMode { | ||
27 | fn default() -> Self { | ||
28 | ViewMode::Month | ||
29 | } | ||
30 | } | ||
31 | |||
32 | struct StatusLine(String, String); | 19 | struct 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 @@ | |||
1 | use std::collections::HashMap; | ||
2 | |||
3 | use chrono::NaiveDate; | ||
4 | use serde::{Deserialize, Serialize}; | ||
5 | |||
6 | use cursive::direction::Direction; | ||
7 | use cursive::event::{Event, EventResult}; | ||
8 | use cursive::{Printer, Vec2}; | ||
9 | |||
10 | use crate::views::ShadowView; | ||
11 | use crate::CONFIGURATION; | ||
12 | |||
13 | pub enum TrackEvent { | ||
14 | Increment, | ||
15 | Decrement, | ||
16 | } | ||
17 | |||
18 | #[derive(Copy, Clone, Debug, Serialize, Deserialize)] | ||
19 | pub struct CustomBool(bool); | ||
20 | |||
21 | use std::fmt; | ||
22 | impl 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 | |||
36 | impl From<bool> for CustomBool { | ||
37 | fn from(b: bool) -> Self { | ||
38 | CustomBool(b) | ||
39 | } | ||
40 | } | ||
41 | |||
42 | pub 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")] | ||
59 | pub 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 | |||
72 | macro_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 | |||
110 | auto_habit_impl!(Count); | ||
111 | auto_habit_impl!(Bit); | ||
112 | |||
113 | #[derive(Debug, Serialize, Deserialize)] | ||
114 | pub struct Count { | ||
115 | name: String, | ||
116 | stats: HashMap<NaiveDate, u32>, | ||
117 | goal: u32, | ||
118 | |||
119 | #[serde(skip)] | ||
120 | view_month_offset: u32, | ||
121 | } | ||
122 | |||
123 | impl 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 | |||
134 | impl 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)] | ||
199 | pub struct Bit { | ||
200 | name: String, | ||
201 | stats: HashMap<NaiveDate, CustomBool>, | ||
202 | goal: CustomBool, | ||
203 | |||
204 | #[serde(skip)] | ||
205 | view_month_offset: u32, | ||
206 | } | ||
207 | |||
208 | impl 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 | |||
219 | impl 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 @@ | |||
1 | use std::collections::HashMap; | ||
2 | |||
3 | use chrono::NaiveDate; | ||
4 | use serde::{Deserialize, Serialize}; | ||
5 | |||
6 | use crate::habit::traits::Habit; | ||
7 | use crate::habit::{TrackEvent, ViewMode}; | ||
8 | use crate::CONFIGURATION; | ||
9 | |||
10 | #[derive(Copy, Clone, Debug, Serialize, Deserialize)] | ||
11 | pub struct CustomBool(bool); | ||
12 | |||
13 | use std::fmt; | ||
14 | impl 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 | |||
28 | impl From<bool> for CustomBool { | ||
29 | fn from(b: bool) -> Self { | ||
30 | CustomBool(b) | ||
31 | } | ||
32 | } | ||
33 | |||
34 | #[derive(Debug, Serialize, Deserialize)] | ||
35 | pub 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 | |||
47 | impl 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 | |||
59 | impl 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 @@ | |||
1 | use std::collections::HashMap; | ||
2 | |||
3 | use chrono::NaiveDate; | ||
4 | use serde::{Deserialize, Serialize}; | ||
5 | |||
6 | use crate::habit::traits::Habit; | ||
7 | use crate::habit::{TrackEvent, ViewMode}; | ||
8 | |||
9 | #[derive(Debug, Serialize, Deserialize)] | ||
10 | pub 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 | |||
22 | impl 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 | |||
34 | impl 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 @@ | |||
1 | use std::collections::HashMap; | ||
2 | |||
3 | use chrono::NaiveDate; | ||
4 | use serde::{Deserialize, Serialize}; | ||
5 | |||
6 | use cursive::direction::Direction; | ||
7 | use cursive::event::{Event, EventResult}; | ||
8 | use cursive::{Printer, Vec2}; | ||
9 | |||
10 | mod traits; | ||
11 | pub use traits::{Habit, HabitWrapper}; | ||
12 | |||
13 | mod count; | ||
14 | pub use count::Count; | ||
15 | |||
16 | mod bit; | ||
17 | pub use bit::Bit; | ||
18 | |||
19 | mod prelude; | ||
20 | pub 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 @@ | |||
1 | use serde::{Deserialize, Serialize}; | ||
2 | |||
3 | pub enum TrackEvent { | ||
4 | Increment, | ||
5 | Decrement, | ||
6 | } | ||
7 | |||
8 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] | ||
9 | pub enum ViewMode { | ||
10 | Day, | ||
11 | Week, | ||
12 | Month, | ||
13 | Year, | ||
14 | } | ||
15 | |||
16 | impl 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 @@ | |||
1 | use chrono::NaiveDate; | ||
2 | use cursive::direction::Direction; | ||
3 | use cursive::event::{Event, EventResult}; | ||
4 | use cursive::{Printer, Vec2}; | ||
5 | |||
6 | use typetag; | ||
7 | |||
8 | use crate::habit::{Bit, Count, TrackEvent, ViewMode}; | ||
9 | use crate::views::ShadowView; | ||
10 | |||
11 | pub 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")] | ||
32 | pub 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 | |||
49 | macro_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 | |||
93 | auto_habit_impl!(Count); | ||
94 | auto_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; | |||
9 | mod utils; | 9 | mod utils; |
10 | mod views; | 10 | mod views; |
11 | 11 | ||
12 | use crate::app::{App, ViewMode}; | 12 | use crate::app::App; |
13 | use crate::command::{open_command_window, Command}; | 13 | use crate::command::{open_command_window, Command}; |
14 | use crate::habit::{Bit, Count, Habit}; | 14 | use crate::habit::{Bit, Count, Habit}; |
15 | use crate::utils::{load_configuration_file, AppConfig}; | 15 | use 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}; | |||
7 | use chrono::prelude::*; | 7 | use chrono::prelude::*; |
8 | use chrono::{Duration, Local, NaiveDate}; | 8 | use chrono::{Duration, Local, NaiveDate}; |
9 | 9 | ||
10 | use crate::habit::{Bit, Count, Habit, TrackEvent}; | 10 | use crate::habit::{Bit, Count, Habit, TrackEvent, ViewMode}; |
11 | |||
11 | use crate::CONFIGURATION; | 12 | use crate::CONFIGURATION; |
12 | 13 | ||
13 | pub trait ShadowView { | 14 | pub 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 { |