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 | |
parent | 3eace50dfb39317fb08e5a95d6126b787c567a17 (diff) |
refactor app.rs into module: app
Diffstat (limited to 'src/app')
-rw-r--r-- | src/app/impl_self.rs | 247 | ||||
-rw-r--r-- | src/app/impl_view.rs | 175 | ||||
-rw-r--r-- | src/app/mod.rs | 27 |
3 files changed, 449 insertions, 0 deletions
diff --git a/src/app/impl_self.rs b/src/app/impl_self.rs new file mode 100644 index 0000000..efed4e0 --- /dev/null +++ b/src/app/impl_self.rs | |||
@@ -0,0 +1,247 @@ | |||
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; | ||
7 | use std::time::Duration; | ||
8 | |||
9 | use chrono::Local; | ||
10 | use cursive::direction::Absolute; | ||
11 | use cursive::Vec2; | ||
12 | use notify::{watcher, RecursiveMode, Watcher}; | ||
13 | |||
14 | use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode}; | ||
15 | use crate::utils; | ||
16 | use crate::Command; | ||
17 | use crate::CONFIGURATION; | ||
18 | |||
19 | use crate::app::{App, StatusLine}; | ||
20 | |||
21 | impl App { | ||
22 | pub fn new() -> Self { | ||
23 | let (tx, rx) = channel(); | ||
24 | let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); | ||
25 | watcher | ||
26 | .watch(utils::auto_habit_file(), RecursiveMode::Recursive) | ||
27 | .unwrap_or_else(|e| { | ||
28 | panic!("Unable to start file watcher: {}", e); | ||
29 | }); | ||
30 | return App { | ||
31 | habits: vec![], | ||
32 | focus: 0, | ||
33 | _file_watcher: watcher, | ||
34 | file_event_recv: rx, | ||
35 | view_month_offset: 0, | ||
36 | }; | ||
37 | } | ||
38 | |||
39 | pub fn add_habit(&mut self, h: Box<dyn HabitWrapper>) { | ||
40 | self.habits.push(h); | ||
41 | } | ||
42 | |||
43 | pub fn delete_by_name(&mut self, name: &str) { | ||
44 | self.habits.retain(|h| h.name() != name); | ||
45 | } | ||
46 | |||
47 | pub fn get_mode(&self) -> ViewMode { | ||
48 | if self.habits.is_empty() { | ||
49 | return ViewMode::Day; | ||
50 | } | ||
51 | return self.habits[self.focus].view_mode(); | ||
52 | } | ||
53 | |||
54 | pub fn set_mode(&mut self, mode: ViewMode) { | ||
55 | if !self.habits.is_empty() { | ||
56 | self.habits[self.focus].set_view_mode(mode); | ||
57 | } | ||
58 | } | ||
59 | |||
60 | pub fn set_view_month_offset(&mut self, offset: u32) { | ||
61 | self.view_month_offset = offset; | ||
62 | for v in self.habits.iter_mut() { | ||
63 | v.set_view_month_offset(offset); | ||
64 | } | ||
65 | } | ||
66 | |||
67 | pub fn sift_backward(&mut self) { | ||
68 | self.view_month_offset += 1; | ||
69 | for v in self.habits.iter_mut() { | ||
70 | v.set_view_month_offset(self.view_month_offset); | ||
71 | } | ||
72 | } | ||
73 | |||
74 | pub fn sift_forward(&mut self) { | ||
75 | if self.view_month_offset > 0 { | ||
76 | self.view_month_offset -= 1; | ||
77 | for v in self.habits.iter_mut() { | ||
78 | v.set_view_month_offset(self.view_month_offset); | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | |||
83 | pub fn set_focus(&mut self, d: Absolute) { | ||
84 | let grid_width = CONFIGURATION.grid_width; | ||
85 | match d { | ||
86 | Absolute::Right => { | ||
87 | if self.focus != self.habits.len() - 1 { | ||
88 | self.focus += 1; | ||
89 | } | ||
90 | } | ||
91 | Absolute::Left => { | ||
92 | if self.focus != 0 { | ||
93 | self.focus -= 1; | ||
94 | } | ||
95 | } | ||
96 | Absolute::Down => { | ||
97 | if self.focus + grid_width < self.habits.len() - 1 { | ||
98 | self.focus += grid_width; | ||
99 | } else { | ||
100 | self.focus = self.habits.len() - 1; | ||
101 | } | ||
102 | } | ||
103 | Absolute::Up => { | ||
104 | if self.focus as isize - grid_width as isize >= 0 { | ||
105 | self.focus -= grid_width; | ||
106 | } else { | ||
107 | self.focus = 0; | ||
108 | } | ||
109 | } | ||
110 | Absolute::None => {} | ||
111 | } | ||
112 | } | ||
113 | |||
114 | pub fn status(&self) -> StatusLine { | ||
115 | let today = chrono::Local::now().naive_utc().date(); | ||
116 | let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::<u32>(); | ||
117 | let total = self.habits.iter().map(|h| h.goal()).sum::<u32>(); | ||
118 | let completed = total - remaining; | ||
119 | |||
120 | let timestamp = if self.view_month_offset == 0 { | ||
121 | format!("{}", Local::now().date().format("%d/%b/%y"),) | ||
122 | } else { | ||
123 | let months = self.view_month_offset; | ||
124 | format!("{}", format!("{} months ago", months),) | ||
125 | }; | ||
126 | |||
127 | StatusLine { | ||
128 | 0: format!( | ||
129 | "Today: {} completed, {} remaining --{}--", | ||
130 | completed, | ||
131 | remaining, | ||
132 | self.get_mode() | ||
133 | ), | ||
134 | 1: timestamp, | ||
135 | } | ||
136 | } | ||
137 | |||
138 | pub fn max_size(&self) -> Vec2 { | ||
139 | let grid_width = CONFIGURATION.grid_width; | ||
140 | let width = { | ||
141 | if self.habits.len() > 0 { | ||
142 | grid_width * CONFIGURATION.view_width | ||
143 | } else { | ||
144 | 0 | ||
145 | } | ||
146 | }; | ||
147 | let height = { | ||
148 | if self.habits.len() > 0 { | ||
149 | (CONFIGURATION.view_height as f64 | ||
150 | * (self.habits.len() as f64 / grid_width as f64).ceil()) | ||
151 | as usize | ||
152 | } else { | ||
153 | 0 | ||
154 | } | ||
155 | }; | ||
156 | Vec2::new(width, height + 2) | ||
157 | } | ||
158 | |||
159 | pub fn load_state() -> Self { | ||
160 | let (regular_f, auto_f) = (utils::habit_file(), utils::auto_habit_file()); | ||
161 | let read_from_file = |file: PathBuf| -> Vec<Box<dyn HabitWrapper>> { | ||
162 | if let Ok(ref mut f) = File::open(file) { | ||
163 | let mut j = String::new(); | ||
164 | f.read_to_string(&mut j); | ||
165 | return serde_json::from_str(&j).unwrap(); | ||
166 | } else { | ||
167 | return Vec::new(); | ||
168 | } | ||
169 | }; | ||
170 | |||
171 | let mut regular = read_from_file(regular_f); | ||
172 | let auto = read_from_file(auto_f); | ||
173 | regular.extend(auto); | ||
174 | return App { | ||
175 | habits: regular, | ||
176 | ..Default::default() | ||
177 | }; | ||
178 | } | ||
179 | |||
180 | // this function does IO | ||
181 | // TODO: convert this into non-blocking async function | ||
182 | pub fn save_state(&self) { | ||
183 | let (regular, auto): (Vec<_>, Vec<_>) = self.habits.iter().partition(|&x| !x.is_auto()); | ||
184 | let (regular_f, auto_f) = (utils::habit_file(), utils::auto_habit_file()); | ||
185 | |||
186 | let write_to_file = |data: Vec<&Box<dyn HabitWrapper>>, file: PathBuf| { | ||
187 | let j = serde_json::to_string_pretty(&data).unwrap(); | ||
188 | match OpenOptions::new() | ||
189 | .write(true) | ||
190 | .create(true) | ||
191 | .truncate(true) | ||
192 | .open(file) | ||
193 | { | ||
194 | Ok(ref mut f) => f.write_all(j.as_bytes()).unwrap(), | ||
195 | Err(_) => panic!("Unable to write!"), | ||
196 | }; | ||
197 | }; | ||
198 | |||
199 | write_to_file(regular, regular_f); | ||
200 | write_to_file(auto, auto_f); | ||
201 | } | ||
202 | |||
203 | pub fn parse_command(&mut self, c: Command) { | ||
204 | match c { | ||
205 | Command::Add(name, goal, auto) => { | ||
206 | let kind = if goal == Some(1) { "bit" } else { "count" }; | ||
207 | if kind == "count" { | ||
208 | self.add_habit(Box::new(Count::new( | ||
209 | name, | ||
210 | goal.unwrap_or(0), | ||
211 | auto.unwrap_or(false), | ||
212 | ))); | ||
213 | } else if kind == "bit" { | ||
214 | self.add_habit(Box::new(Bit::new(name, auto.unwrap_or(false)))); | ||
215 | } | ||
216 | } | ||
217 | Command::Delete(name) => { | ||
218 | self.delete_by_name(&name); | ||
219 | self.focus = 0; | ||
220 | } | ||
221 | Command::TrackUp(name) => { | ||
222 | let target_habit = self | ||
223 | .habits | ||
224 | .iter_mut() | ||
225 | .find(|x| x.name() == name && x.is_auto()); | ||
226 | if let Some(h) = target_habit { | ||
227 | h.modify(Local::now().naive_utc().date(), TrackEvent::Increment); | ||
228 | } | ||
229 | } | ||
230 | Command::TrackDown(name) => { | ||
231 | let target_habit = self | ||
232 | .habits | ||
233 | .iter_mut() | ||
234 | .find(|x| x.name() == name && x.is_auto()); | ||
235 | if let Some(h) = target_habit { | ||
236 | h.modify(Local::now().naive_utc().date(), TrackEvent::Decrement); | ||
237 | } | ||
238 | } | ||
239 | Command::Quit => self.save_state(), | ||
240 | Command::MonthNext => self.sift_forward(), | ||
241 | Command::MonthPrev => self.sift_backward(), | ||
242 | _ => { | ||
243 | eprintln!("UNKNOWN COMMAND!"); | ||
244 | } | ||
245 | } | ||
246 | } | ||
247 | } | ||
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 | } | ||