aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/impl_self.rs247
-rw-r--r--src/app/impl_view.rs175
-rw-r--r--src/app/mod.rs27
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 @@
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;
7use std::time::Duration;
8
9use chrono::Local;
10use cursive::direction::Absolute;
11use cursive::Vec2;
12use notify::{watcher, RecursiveMode, Watcher};
13
14use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode};
15use crate::utils;
16use crate::Command;
17use crate::CONFIGURATION;
18
19use crate::app::{App, StatusLine};
20
21impl 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 @@
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}