diff options
author | Akshay <[email protected]> | 2020-07-18 10:05:59 +0100 |
---|---|---|
committer | Akshay <[email protected]> | 2020-07-18 10:05:59 +0100 |
commit | 7740a2ad558eb289e9d8c0b33fe43453942398e0 (patch) | |
tree | 7537d4b2aafe941ce1bca5b66c95e62d7a06f2f6 /src/app.rs | |
parent | 3eace50dfb39317fb08e5a95d6126b787c567a17 (diff) |
refactor app.rs into module: app
Diffstat (limited to 'src/app.rs')
-rw-r--r-- | src/app.rs | 425 |
1 files changed, 0 insertions, 425 deletions
diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 52c7f5f..0000000 --- a/src/app.rs +++ /dev/null | |||
@@ -1,425 +0,0 @@ | |||
1 | use std::default::Default; | ||
2 | use std::f64; | ||
3 | use std::fs::{File, OpenOptions}; | ||
4 | use std::io::prelude::*; | ||
5 | use std::path::PathBuf; | ||
6 | use std::sync::mpsc::{channel, Receiver}; | ||
7 | use std::time::Duration; | ||
8 | |||
9 | use chrono::Local; | ||
10 | use cursive::direction::{Absolute, Direction}; | ||
11 | use cursive::event::{Event, EventResult, Key}; | ||
12 | use cursive::view::View; | ||
13 | use cursive::{Printer, Vec2}; | ||
14 | use notify::{watcher, DebouncedEvent, INotifyWatcher, RecursiveMode, Watcher}; | ||
15 | |||
16 | use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode}; | ||
17 | use crate::utils; | ||
18 | use crate::Command; | ||
19 | use crate::CONFIGURATION; | ||
20 | |||
21 | struct StatusLine(String, String); | ||
22 | |||
23 | pub struct App { | ||
24 | // holds app data | ||
25 | habits: Vec<Box<dyn HabitWrapper>>, | ||
26 | |||
27 | _file_watcher: INotifyWatcher, | ||
28 | file_event_recv: Receiver<DebouncedEvent>, | ||
29 | focus: usize, | ||
30 | view_month_offset: u32, | ||
31 | } | ||
32 | |||
33 | impl Default for App { | ||
34 | fn default() -> Self { | ||
35 | App::new() | ||
36 | } | ||
37 | } | ||
38 | |||
39 | impl App { | ||
40 | pub fn new() -> Self { | ||
41 | let (tx, rx) = channel(); | ||
42 | let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); | ||
43 | watcher | ||
44 | .watch(utils::auto_habit_file(), RecursiveMode::Recursive) | ||
45 | .unwrap_or_else(|e| { | ||
46 | panic!("Unable to start file watcher: {}", e); | ||
47 | }); | ||
48 | return App { | ||
49 | habits: vec![], | ||
50 | focus: 0, | ||
51 | _file_watcher: watcher, | ||
52 | file_event_recv: rx, | ||
53 | view_month_offset: 0, | ||
54 | }; | ||
55 | } | ||
56 | |||
57 | pub fn add_habit(&mut self, h: Box<dyn HabitWrapper>) { | ||
58 | self.habits.push(h); | ||
59 | } | ||
60 | |||
61 | pub fn delete_by_name(&mut self, name: &str) { | ||
62 | self.habits.retain(|h| h.name() != name); | ||
63 | } | ||
64 | |||
65 | pub fn get_mode(&self) -> ViewMode { | ||
66 | if self.habits.is_empty() { | ||
67 | return ViewMode::Day; | ||
68 | } | ||
69 | return self.habits[self.focus].view_mode(); | ||
70 | } | ||
71 | |||
72 | pub fn set_mode(&mut self, mode: ViewMode) { | ||
73 | if !self.habits.is_empty() { | ||
74 | self.habits[self.focus].set_view_mode(mode); | ||
75 | } | ||
76 | } | ||
77 | |||
78 | pub fn set_view_month_offset(&mut self, offset: u32) { | ||
79 | self.view_month_offset = offset; | ||
80 | for v in self.habits.iter_mut() { | ||
81 | v.set_view_month_offset(offset); | ||
82 | } | ||
83 | } | ||
84 | |||
85 | pub fn sift_backward(&mut self) { | ||
86 | self.view_month_offset += 1; | ||
87 | for v in self.habits.iter_mut() { | ||
88 | v.set_view_month_offset(self.view_month_offset); | ||
89 | } | ||
90 | } | ||
91 | |||
92 | pub fn sift_forward(&mut self) { | ||
93 | if self.view_month_offset > 0 { | ||
94 | self.view_month_offset -= 1; | ||
95 | for v in self.habits.iter_mut() { | ||
96 | v.set_view_month_offset(self.view_month_offset); | ||
97 | } | ||
98 | } | ||
99 | } | ||
100 | |||
101 | fn set_focus(&mut self, d: Absolute) { | ||
102 | let grid_width = CONFIGURATION.grid_width; | ||
103 | match d { | ||
104 | Absolute::Right => { | ||
105 | if self.focus != self.habits.len() - 1 { | ||
106 | self.focus += 1; | ||
107 | } | ||
108 | } | ||
109 | Absolute::Left => { | ||
110 | if self.focus != 0 { | ||
111 | self.focus -= 1; | ||
112 | } | ||
113 | } | ||
114 | Absolute::Down => { | ||
115 | if self.focus + grid_width < self.habits.len() - 1 { | ||
116 | self.focus += grid_width; | ||
117 | } else { | ||
118 | self.focus = self.habits.len() - 1; | ||
119 | } | ||
120 | } | ||
121 | Absolute::Up => { | ||
122 | if self.focus as isize - grid_width as isize >= 0 { | ||
123 | self.focus -= grid_width; | ||
124 | } else { | ||
125 | self.focus = 0; | ||
126 | } | ||
127 | } | ||
128 | Absolute::None => {} | ||
129 | } | ||
130 | } | ||
131 | |||
132 | fn status(&self) -> StatusLine { | ||
133 | let today = chrono::Local::now().naive_utc().date(); | ||
134 | let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::<u32>(); | ||
135 | let total = self.habits.iter().map(|h| h.goal()).sum::<u32>(); | ||
136 | let completed = total - remaining; | ||
137 | |||
138 | let timestamp = if self.view_month_offset == 0 { | ||
139 | format!("{}", Local::now().date().format("%d/%b/%y"),) | ||
140 | } else { | ||
141 | let months = self.view_month_offset; | ||
142 | format!("{}", format!("{} months ago", months),) | ||
143 | }; | ||
144 | |||
145 | StatusLine { | ||
146 | 0: format!( | ||
147 | "Today: {} completed, {} remaining --{}--", | ||
148 | completed, | ||
149 | remaining, | ||
150 | self.get_mode() | ||
151 | ), | ||
152 | 1: timestamp, | ||
153 | } | ||
154 | } | ||
155 | |||
156 | fn max_size(&self) -> Vec2 { | ||
157 | let grid_width = CONFIGURATION.grid_width; | ||
158 | let width = { | ||
159 | if self.habits.len() > 0 { | ||
160 | grid_width * CONFIGURATION.view_width | ||
161 | } else { | ||
162 | 0 | ||
163 | } | ||
164 | }; | ||
165 | let height = { | ||
166 | if self.habits.len() > 0 { | ||
167 | (CONFIGURATION.view_height as f64 | ||
168 | * (self.habits.len() as f64 / grid_width as f64).ceil()) | ||
169 | as usize | ||
170 | } else { | ||
171 | 0 | ||
172 | } | ||
173 | }; | ||
174 | Vec2::new(width, height + 2) | ||
175 | } | ||
176 | |||
177 | pub fn load_state() -> Self { | ||
178 | let (regular_f, auto_f) = (utils::habit_file(), utils::auto_habit_file()); | ||
179 | let read_from_file = |file: PathBuf| -> Vec<Box<dyn HabitWrapper>> { | ||
180 | if let Ok(ref mut f) = File::open(file) { | ||
181 | let mut j = String::new(); | ||
182 | f.read_to_string(&mut j); | ||
183 | return serde_json::from_str(&j).unwrap(); | ||
184 | } else { | ||
185 | return Vec::new(); | ||
186 | } | ||
187 | }; | ||
188 | |||
189 | let mut regular = read_from_file(regular_f); | ||
190 | let auto = read_from_file(auto_f); | ||
191 | regular.extend(auto); | ||
192 | return App { | ||
193 | habits: regular, | ||
194 | ..Default::default() | ||
195 | }; | ||
196 | } | ||
197 | |||
198 | // this function does IO | ||
199 | // TODO: convert this into non-blocking async function | ||
200 | pub fn save_state(&self) { | ||
201 | let (regular, auto): (Vec<_>, Vec<_>) = self.habits.iter().partition(|&x| !x.is_auto()); | ||
202 | let (regular_f, auto_f) = (utils::habit_file(), utils::auto_habit_file()); | ||
203 | |||
204 | let write_to_file = |data: Vec<&Box<dyn HabitWrapper>>, file: PathBuf| { | ||
205 | let j = serde_json::to_string_pretty(&data).unwrap(); | ||
206 | match OpenOptions::new() | ||
207 | .write(true) | ||
208 | .create(true) | ||
209 | .truncate(true) | ||
210 | .open(file) | ||
211 | { | ||
212 | Ok(ref mut f) => f.write_all(j.as_bytes()).unwrap(), | ||
213 | Err(_) => panic!("Unable to write!"), | ||
214 | }; | ||
215 | }; | ||
216 | |||
217 | write_to_file(regular, regular_f); | ||
218 | write_to_file(auto, auto_f); | ||
219 | } | ||
220 | |||
221 | pub fn parse_command(&mut self, c: Command) { | ||
222 | match c { | ||
223 | Command::Add(name, goal, auto) => { | ||
224 | let kind = if goal == Some(1) { "bit" } else { "count" }; | ||
225 | if kind == "count" { | ||
226 | self.add_habit(Box::new(Count::new( | ||
227 | name, | ||
228 | goal.unwrap_or(0), | ||
229 | auto.unwrap_or(false), | ||
230 | ))); | ||
231 | } else if kind == "bit" { | ||
232 | self.add_habit(Box::new(Bit::new(name, auto.unwrap_or(false)))); | ||
233 | } | ||
234 | } | ||
235 | Command::Delete(name) => { | ||
236 | self.delete_by_name(&name); | ||
237 | self.focus = 0; | ||
238 | } | ||
239 | Command::TrackUp(name) => { | ||
240 | let target_habit = self | ||
241 | .habits | ||
242 | .iter_mut() | ||
243 | .find(|x| x.name() == name && x.is_auto()); | ||
244 | if let Some(h) = target_habit { | ||
245 | h.modify(Local::now().naive_utc().date(), TrackEvent::Increment); | ||
246 | } | ||
247 | } | ||
248 | Command::TrackDown(name) => { | ||
249 | let target_habit = self | ||
250 | .habits | ||
251 | .iter_mut() | ||
252 | .find(|x| x.name() == name && x.is_auto()); | ||
253 | if let Some(h) = target_habit { | ||
254 | h.modify(Local::now().naive_utc().date(), TrackEvent::Decrement); | ||
255 | } | ||
256 | } | ||
257 | Command::Quit => self.save_state(), | ||
258 | Command::MonthNext => self.sift_forward(), | ||
259 | Command::MonthPrev => self.sift_backward(), | ||
260 | _ => { | ||
261 | eprintln!("UNKNOWN COMMAND!"); | ||
262 | } | ||
263 | } | ||
264 | } | ||
265 | } | ||
266 | |||
267 | impl View for App { | ||
268 | fn draw(&self, printer: &Printer) { | ||
269 | let grid_width = CONFIGURATION.grid_width; | ||
270 | let view_width = CONFIGURATION.view_width; | ||
271 | let view_height = CONFIGURATION.view_height; | ||
272 | let mut offset = Vec2::zero(); | ||
273 | for (idx, i) in self.habits.iter().enumerate() { | ||
274 | if idx >= grid_width && idx % grid_width == 0 { | ||
275 | offset = offset.map_y(|y| y + view_height).map_x(|_| 0); | ||
276 | } | ||
277 | i.draw(&printer.offset(offset).focused(self.focus == idx)); | ||
278 | offset = offset.map_x(|x| x + view_width + 2); | ||
279 | } | ||
280 | |||
281 | offset = offset.map_x(|_| 0).map_y(|_| self.max_size().y - 2); | ||
282 | |||
283 | let status = self.status(); | ||
284 | printer.print(offset, &status.0); // left status | ||
285 | |||
286 | let full = self.max_size().x; | ||
287 | offset = offset.map_x(|_| full - status.1.len()); | ||
288 | printer.print(offset, &status.1); // right status | ||
289 | } | ||
290 | |||
291 | fn required_size(&mut self, _: Vec2) -> Vec2 { | ||
292 | let grid_width = CONFIGURATION.grid_width; | ||
293 | let view_width = CONFIGURATION.view_width; | ||
294 | let view_height = CONFIGURATION.view_height; | ||
295 | let width = { | ||
296 | if self.habits.len() > 0 { | ||
297 | grid_width * (view_width + 2) | ||
298 | } else { | ||
299 | 0 | ||
300 | } | ||
301 | }; | ||
302 | let height = { | ||
303 | if self.habits.len() > 0 { | ||
304 | (view_height as f64 * (self.habits.len() as f64 / grid_width as f64).ceil()) | ||
305 | as usize | ||
306 | + 2 // to acoomodate statusline and commandline | ||
307 | } else { | ||
308 | 0 | ||
309 | } | ||
310 | }; | ||
311 | Vec2::new(width, height) | ||
312 | } | ||
313 | |||
314 | fn take_focus(&mut self, _: Direction) -> bool { | ||
315 | false | ||
316 | } | ||
317 | |||
318 | fn on_event(&mut self, e: Event) -> EventResult { | ||
319 | match self.file_event_recv.try_recv() { | ||
320 | Ok(DebouncedEvent::Write(_)) => { | ||
321 | let read_from_file = |file: PathBuf| -> Vec<Box<dyn HabitWrapper>> { | ||
322 | if let Ok(ref mut f) = File::open(file) { | ||
323 | let mut j = String::new(); | ||
324 | f.read_to_string(&mut j); | ||
325 | return serde_json::from_str(&j).unwrap(); | ||
326 | } else { | ||
327 | return Vec::new(); | ||
328 | } | ||
329 | }; | ||
330 | let auto = read_from_file(utils::auto_habit_file()); | ||
331 | self.habits.retain(|x| !x.is_auto()); | ||
332 | self.habits.extend(auto); | ||
333 | } | ||
334 | _ => {} | ||
335 | }; | ||
336 | match e { | ||
337 | Event::Key(Key::Right) | Event::Key(Key::Tab) | Event::Char('l') => { | ||
338 | self.set_focus(Absolute::Right); | ||
339 | return EventResult::Consumed(None); | ||
340 | } | ||
341 | Event::Key(Key::Left) | Event::Shift(Key::Tab) | Event::Char('h') => { | ||
342 | self.set_focus(Absolute::Left); | ||
343 | return EventResult::Consumed(None); | ||
344 | } | ||
345 | Event::Key(Key::Up) | Event::Char('k') => { | ||
346 | self.set_focus(Absolute::Up); | ||
347 | return EventResult::Consumed(None); | ||
348 | } | ||
349 | Event::Key(Key::Down) | Event::Char('j') => { | ||
350 | self.set_focus(Absolute::Down); | ||
351 | return EventResult::Consumed(None); | ||
352 | } | ||
353 | Event::Char('d') => { | ||
354 | if self.habits.is_empty() { | ||
355 | return EventResult::Consumed(None); | ||
356 | } | ||
357 | self.habits.remove(self.focus); | ||
358 | self.focus = self.focus.checked_sub(1).unwrap_or(0); | ||
359 | return EventResult::Consumed(None); | ||
360 | } | ||
361 | Event::Char('w') => { | ||
362 | // helper bind to test write to file | ||
363 | let j = serde_json::to_string_pretty(&self.habits).unwrap(); | ||
364 | let mut file = File::create("foo.txt").unwrap(); | ||
365 | file.write_all(j.as_bytes()).unwrap(); | ||
366 | return EventResult::Consumed(None); | ||
367 | } | ||
368 | Event::Char('q') => { | ||
369 | self.save_state(); | ||
370 | return EventResult::with_cb(|s| s.quit()); | ||
371 | } | ||
372 | Event::Char('v') => { | ||
373 | if self.habits.is_empty() { | ||
374 | return EventResult::Consumed(None); | ||
375 | } | ||
376 | if self.habits[self.focus].view_mode() == ViewMode::Week { | ||
377 | self.set_mode(ViewMode::Day) | ||
378 | } else { | ||
379 | self.set_mode(ViewMode::Week) | ||
380 | } | ||
381 | return EventResult::Consumed(None); | ||
382 | } | ||
383 | Event::Char('V') => { | ||
384 | for habit in self.habits.iter_mut() { | ||
385 | habit.set_view_mode(ViewMode::Week); | ||
386 | } | ||
387 | return EventResult::Consumed(None); | ||
388 | } | ||
389 | Event::Key(Key::Esc) => { | ||
390 | for habit in self.habits.iter_mut() { | ||
391 | habit.set_view_mode(ViewMode::Day); | ||
392 | } | ||
393 | return EventResult::Consumed(None); | ||
394 | } | ||
395 | |||
396 | /* We want sifting to be an app level function, | ||
397 | * that later trickles down into each habit | ||
398 | * */ | ||
399 | Event::Char(']') => { | ||
400 | self.sift_forward(); | ||
401 | return EventResult::Consumed(None); | ||
402 | } | ||
403 | Event::Char('[') => { | ||
404 | self.sift_backward(); | ||
405 | return EventResult::Consumed(None); | ||
406 | } | ||
407 | Event::Char('}') => { | ||
408 | self.set_view_month_offset(0); | ||
409 | return EventResult::Consumed(None); | ||
410 | } | ||
411 | |||
412 | /* Every keybind that is not caught by App trickles | ||
413 | * down to the focused Habit We sift back to today | ||
414 | * before performing any action, "refocusing" the cursor | ||
415 | * */ | ||
416 | _ => { | ||
417 | if self.habits.is_empty() { | ||
418 | return EventResult::Ignored; | ||
419 | } | ||
420 | self.set_view_month_offset(0); | ||
421 | self.habits[self.focus].on_event(e) | ||
422 | } | ||
423 | } | ||
424 | } | ||
425 | } | ||