aboutsummaryrefslogtreecommitdiff
path: root/src/app.rs
diff options
context:
space:
mode:
authorAkshay <[email protected]>2020-07-18 10:05:59 +0100
committerAkshay <[email protected]>2020-07-18 10:05:59 +0100
commit7740a2ad558eb289e9d8c0b33fe43453942398e0 (patch)
tree7537d4b2aafe941ce1bca5b66c95e62d7a06f2f6 /src/app.rs
parent3eace50dfb39317fb08e5a95d6126b787c567a17 (diff)
refactor app.rs into module: app
Diffstat (limited to 'src/app.rs')
-rw-r--r--src/app.rs425
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 @@
1use std::default::Default;
2use std::f64;
3use std::fs::{File, OpenOptions};
4use std::io::prelude::*;
5use std::path::PathBuf;
6use std::sync::mpsc::{channel, Receiver};
7use std::time::Duration;
8
9use chrono::Local;
10use cursive::direction::{Absolute, Direction};
11use cursive::event::{Event, EventResult, Key};
12use cursive::view::View;
13use cursive::{Printer, Vec2};
14use notify::{watcher, DebouncedEvent, INotifyWatcher, RecursiveMode, Watcher};
15
16use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode};
17use crate::utils;
18use crate::Command;
19use crate::CONFIGURATION;
20
21struct StatusLine(String, String);
22
23pub 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
33impl Default for App {
34 fn default() -> Self {
35 App::new()
36 }
37}
38
39impl 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
267impl 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}