diff options
-rw-r--r-- | src/app/impl_self.rs (renamed from src/app.rs) | 194 | ||||
-rw-r--r-- | src/app/impl_view.rs | 175 | ||||
-rw-r--r-- | src/app/mod.rs | 27 |
3 files changed, 210 insertions, 186 deletions
diff --git a/src/app.rs b/src/app/impl_self.rs index 52c7f5f..efed4e0 100644 --- a/src/app.rs +++ b/src/app/impl_self.rs | |||
@@ -3,38 +3,20 @@ use std::f64; | |||
3 | use std::fs::{File, OpenOptions}; | 3 | use std::fs::{File, OpenOptions}; |
4 | use std::io::prelude::*; | 4 | use std::io::prelude::*; |
5 | use std::path::PathBuf; | 5 | use std::path::PathBuf; |
6 | use std::sync::mpsc::{channel, Receiver}; | 6 | use std::sync::mpsc::channel; |
7 | use std::time::Duration; | 7 | use std::time::Duration; |
8 | 8 | ||
9 | use chrono::Local; | 9 | use chrono::Local; |
10 | use cursive::direction::{Absolute, Direction}; | 10 | use cursive::direction::Absolute; |
11 | use cursive::event::{Event, EventResult, Key}; | 11 | use cursive::Vec2; |
12 | use cursive::view::View; | 12 | use notify::{watcher, RecursiveMode, Watcher}; |
13 | use cursive::{Printer, Vec2}; | ||
14 | use notify::{watcher, DebouncedEvent, INotifyWatcher, RecursiveMode, Watcher}; | ||
15 | 13 | ||
16 | use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode}; | 14 | use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode}; |
17 | use crate::utils; | 15 | use crate::utils; |
18 | use crate::Command; | 16 | use crate::Command; |
19 | use crate::CONFIGURATION; | 17 | use crate::CONFIGURATION; |
20 | 18 | ||
21 | struct StatusLine(String, String); | 19 | use crate::app::{App, StatusLine}; |
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 | 20 | ||
39 | impl App { | 21 | impl App { |
40 | pub fn new() -> Self { | 22 | pub fn new() -> Self { |
@@ -98,7 +80,7 @@ impl App { | |||
98 | } | 80 | } |
99 | } | 81 | } |
100 | 82 | ||
101 | fn set_focus(&mut self, d: Absolute) { | 83 | pub fn set_focus(&mut self, d: Absolute) { |
102 | let grid_width = CONFIGURATION.grid_width; | 84 | let grid_width = CONFIGURATION.grid_width; |
103 | match d { | 85 | match d { |
104 | Absolute::Right => { | 86 | Absolute::Right => { |
@@ -129,7 +111,7 @@ impl App { | |||
129 | } | 111 | } |
130 | } | 112 | } |
131 | 113 | ||
132 | fn status(&self) -> StatusLine { | 114 | pub fn status(&self) -> StatusLine { |
133 | let today = chrono::Local::now().naive_utc().date(); | 115 | let today = chrono::Local::now().naive_utc().date(); |
134 | let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::<u32>(); | 116 | let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::<u32>(); |
135 | let total = self.habits.iter().map(|h| h.goal()).sum::<u32>(); | 117 | let total = self.habits.iter().map(|h| h.goal()).sum::<u32>(); |
@@ -153,7 +135,7 @@ impl App { | |||
153 | } | 135 | } |
154 | } | 136 | } |
155 | 137 | ||
156 | fn max_size(&self) -> Vec2 { | 138 | pub fn max_size(&self) -> Vec2 { |
157 | let grid_width = CONFIGURATION.grid_width; | 139 | let grid_width = CONFIGURATION.grid_width; |
158 | let width = { | 140 | let width = { |
159 | if self.habits.len() > 0 { | 141 | if self.habits.len() > 0 { |
@@ -263,163 +245,3 @@ impl App { | |||
263 | } | 245 | } |
264 | } | 246 | } |
265 | } | 247 | } |
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 | } | ||
diff --git a/src/app/impl_view.rs b/src/app/impl_view.rs new file mode 100644 index 0000000..904403b --- /dev/null +++ b/src/app/impl_view.rs | |||
@@ -0,0 +1,175 @@ | |||
1 | use std::f64; | ||
2 | use std::fs::File; | ||
3 | use std::io::prelude::*; | ||
4 | use std::path::PathBuf; | ||
5 | |||
6 | use cursive::direction::{Absolute, Direction}; | ||
7 | use cursive::event::{Event, EventResult, Key}; | ||
8 | use cursive::view::View; | ||
9 | use cursive::{Printer, Vec2}; | ||
10 | use notify::DebouncedEvent; | ||
11 | |||
12 | use crate::app::App; | ||
13 | use crate::habit::{HabitWrapper, ViewMode}; | ||
14 | use crate::utils; | ||
15 | use crate::CONFIGURATION; | ||
16 | |||
17 | impl View for App { | ||
18 | fn draw(&self, printer: &Printer) { | ||
19 | let grid_width = CONFIGURATION.grid_width; | ||
20 | let view_width = CONFIGURATION.view_width; | ||
21 | let view_height = CONFIGURATION.view_height; | ||
22 | let mut offset = Vec2::zero(); | ||
23 | for (idx, habit) in self.habits.iter().enumerate() { | ||
24 | if idx >= grid_width && idx % grid_width == 0 { | ||
25 | offset = offset.map_y(|y| y + view_height).map_x(|_| 0); | ||
26 | } | ||
27 | habit.draw(&printer.offset(offset).focused(self.focus == idx)); | ||
28 | offset = offset.map_x(|x| x + view_width + 2); | ||
29 | } | ||
30 | |||
31 | offset = offset.map_x(|_| 0).map_y(|_| self.max_size().y - 2); | ||
32 | |||
33 | let status = self.status(); | ||
34 | printer.print(offset, &status.0); // left status | ||
35 | |||
36 | let full = self.max_size().x; | ||
37 | offset = offset.map_x(|_| full - status.1.len()); | ||
38 | printer.print(offset, &status.1); // right status | ||
39 | } | ||
40 | |||
41 | fn required_size(&mut self, _: Vec2) -> Vec2 { | ||
42 | let grid_width = CONFIGURATION.grid_width; | ||
43 | let view_width = CONFIGURATION.view_width; | ||
44 | let view_height = CONFIGURATION.view_height; | ||
45 | let width = { | ||
46 | if self.habits.len() > 0 { | ||
47 | grid_width * (view_width + 2) | ||
48 | } else { | ||
49 | 0 | ||
50 | } | ||
51 | }; | ||
52 | let height = { | ||
53 | if self.habits.len() > 0 { | ||
54 | (view_height as f64 * (self.habits.len() as f64 / grid_width as f64).ceil()) | ||
55 | as usize | ||
56 | + 2 // to acoomodate statusline and message line | ||
57 | } else { | ||
58 | 0 | ||
59 | } | ||
60 | }; | ||
61 | Vec2::new(width, height) | ||
62 | } | ||
63 | |||
64 | fn take_focus(&mut self, _: Direction) -> bool { | ||
65 | false | ||
66 | } | ||
67 | |||
68 | fn on_event(&mut self, e: Event) -> EventResult { | ||
69 | match self.file_event_recv.try_recv() { | ||
70 | Ok(DebouncedEvent::Write(_)) => { | ||
71 | let read_from_file = |file: PathBuf| -> Vec<Box<dyn HabitWrapper>> { | ||
72 | if let Ok(ref mut f) = File::open(file) { | ||
73 | let mut j = String::new(); | ||
74 | f.read_to_string(&mut j); | ||
75 | return serde_json::from_str(&j).unwrap(); | ||
76 | } else { | ||
77 | return Vec::new(); | ||
78 | } | ||
79 | }; | ||
80 | let auto = read_from_file(utils::auto_habit_file()); | ||
81 | self.habits.retain(|x| !x.is_auto()); | ||
82 | self.habits.extend(auto); | ||
83 | } | ||
84 | _ => {} | ||
85 | }; | ||
86 | match e { | ||
87 | Event::Key(Key::Right) | Event::Key(Key::Tab) | Event::Char('l') => { | ||
88 | self.set_focus(Absolute::Right); | ||
89 | return EventResult::Consumed(None); | ||
90 | } | ||
91 | Event::Key(Key::Left) | Event::Shift(Key::Tab) | Event::Char('h') => { | ||
92 | self.set_focus(Absolute::Left); | ||
93 | return EventResult::Consumed(None); | ||
94 | } | ||
95 | Event::Key(Key::Up) | Event::Char('k') => { | ||
96 | self.set_focus(Absolute::Up); | ||
97 | return EventResult::Consumed(None); | ||
98 | } | ||
99 | Event::Key(Key::Down) | Event::Char('j') => { | ||
100 | self.set_focus(Absolute::Down); | ||
101 | return EventResult::Consumed(None); | ||
102 | } | ||
103 | Event::Char('d') => { | ||
104 | if self.habits.is_empty() { | ||
105 | return EventResult::Consumed(None); | ||
106 | } | ||
107 | self.habits.remove(self.focus); | ||
108 | self.focus = self.focus.checked_sub(1).unwrap_or(0); | ||
109 | return EventResult::Consumed(None); | ||
110 | } | ||
111 | Event::Char('w') => { | ||
112 | // helper bind to test write to file | ||
113 | let j = serde_json::to_string_pretty(&self.habits).unwrap(); | ||
114 | let mut file = File::create("foo.txt").unwrap(); | ||
115 | file.write_all(j.as_bytes()).unwrap(); | ||
116 | return EventResult::Consumed(None); | ||
117 | } | ||
118 | Event::Char('q') => { | ||
119 | self.save_state(); | ||
120 | return EventResult::with_cb(|s| s.quit()); | ||
121 | } | ||
122 | Event::Char('v') => { | ||
123 | if self.habits.is_empty() { | ||
124 | return EventResult::Consumed(None); | ||
125 | } | ||
126 | if self.habits[self.focus].view_mode() == ViewMode::Week { | ||
127 | self.set_mode(ViewMode::Day) | ||
128 | } else { | ||
129 | self.set_mode(ViewMode::Week) | ||
130 | } | ||
131 | return EventResult::Consumed(None); | ||
132 | } | ||
133 | Event::Char('V') => { | ||
134 | for habit in self.habits.iter_mut() { | ||
135 | habit.set_view_mode(ViewMode::Week); | ||
136 | } | ||
137 | return EventResult::Consumed(None); | ||
138 | } | ||
139 | Event::Key(Key::Esc) => { | ||
140 | for habit in self.habits.iter_mut() { | ||
141 | habit.set_view_mode(ViewMode::Day); | ||
142 | } | ||
143 | return EventResult::Consumed(None); | ||
144 | } | ||
145 | |||
146 | /* We want sifting to be an app level function, | ||
147 | * that later trickles down into each habit | ||
148 | * */ | ||
149 | Event::Char(']') => { | ||
150 | self.sift_forward(); | ||
151 | return EventResult::Consumed(None); | ||
152 | } | ||
153 | Event::Char('[') => { | ||
154 | self.sift_backward(); | ||
155 | return EventResult::Consumed(None); | ||
156 | } | ||
157 | Event::Char('}') => { | ||
158 | self.set_view_month_offset(0); | ||
159 | return EventResult::Consumed(None); | ||
160 | } | ||
161 | |||
162 | /* Every keybind that is not caught by App trickles | ||
163 | * down to the focused habit. We sift back to today | ||
164 | * before performing any action, "refocusing" the cursor | ||
165 | * */ | ||
166 | _ => { | ||
167 | if self.habits.is_empty() { | ||
168 | return EventResult::Ignored; | ||
169 | } | ||
170 | self.set_view_month_offset(0); | ||
171 | self.habits[self.focus].on_event(e) | ||
172 | } | ||
173 | } | ||
174 | } | ||
175 | } | ||
diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..f00c936 --- /dev/null +++ b/src/app/mod.rs | |||
@@ -0,0 +1,27 @@ | |||
1 | use std::default::Default; | ||
2 | use std::sync::mpsc::Receiver; | ||
3 | |||
4 | use notify::{DebouncedEvent, INotifyWatcher}; | ||
5 | |||
6 | use crate::habit::HabitWrapper; | ||
7 | |||
8 | mod impl_self; | ||
9 | mod impl_view; | ||
10 | |||
11 | pub struct StatusLine(String, String); | ||
12 | |||
13 | pub struct App { | ||
14 | // holds app data | ||
15 | habits: Vec<Box<dyn HabitWrapper>>, | ||
16 | |||
17 | _file_watcher: INotifyWatcher, | ||
18 | file_event_recv: Receiver<DebouncedEvent>, | ||
19 | focus: usize, | ||
20 | view_month_offset: u32, | ||
21 | } | ||
22 | |||
23 | impl Default for App { | ||
24 | fn default() -> Self { | ||
25 | App::new() | ||
26 | } | ||
27 | } | ||