aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/impl_self.rs (renamed from src/app.rs)194
-rw-r--r--src/app/impl_view.rs175
-rw-r--r--src/app/mod.rs27
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;
3use std::fs::{File, OpenOptions}; 3use std::fs::{File, OpenOptions};
4use std::io::prelude::*; 4use std::io::prelude::*;
5use std::path::PathBuf; 5use std::path::PathBuf;
6use std::sync::mpsc::{channel, Receiver}; 6use std::sync::mpsc::channel;
7use std::time::Duration; 7use std::time::Duration;
8 8
9use chrono::Local; 9use chrono::Local;
10use cursive::direction::{Absolute, Direction}; 10use cursive::direction::Absolute;
11use cursive::event::{Event, EventResult, Key}; 11use cursive::Vec2;
12use cursive::view::View; 12use notify::{watcher, RecursiveMode, Watcher};
13use cursive::{Printer, Vec2};
14use notify::{watcher, DebouncedEvent, INotifyWatcher, RecursiveMode, Watcher};
15 13
16use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode}; 14use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode};
17use crate::utils; 15use crate::utils;
18use crate::Command; 16use crate::Command;
19use crate::CONFIGURATION; 17use crate::CONFIGURATION;
20 18
21struct StatusLine(String, String); 19use crate::app::{App, StatusLine};
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 20
39impl App { 21impl 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
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}
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 @@
1use std::f64;
2use std::fs::File;
3use std::io::prelude::*;
4use std::path::PathBuf;
5
6use cursive::direction::{Absolute, Direction};
7use cursive::event::{Event, EventResult, Key};
8use cursive::view::View;
9use cursive::{Printer, Vec2};
10use notify::DebouncedEvent;
11
12use crate::app::App;
13use crate::habit::{HabitWrapper, ViewMode};
14use crate::utils;
15use crate::CONFIGURATION;
16
17impl 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 @@
1use std::default::Default;
2use std::sync::mpsc::Receiver;
3
4use notify::{DebouncedEvent, INotifyWatcher};
5
6use crate::habit::HabitWrapper;
7
8mod impl_self;
9mod impl_view;
10
11pub struct StatusLine(String, String);
12
13pub 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
23impl Default for App {
24 fn default() -> Self {
25 App::new()
26 }
27}