1use crate::{Command, Productivity, Reminder, Repeat, Task};
2use chrono::Duration;
3use chrono::Local;
4use chrono_humanize::HumanTime;
5use std::env;
6use std::fmt;
7use std::fs;
8use std::fs::File;
9use std::io::{self, Read, Write};
10use std::process;
11use termion::color;
12use termion::terminal_size;
13
14static VERSION: &str = env!("CARGO_PKG_VERSION");
16
17#[derive(Default)]
19pub struct Mind {
20 tasks: Vec<Task>,
21 reminders: Vec<Reminder>,
22 focused: Option<usize>,
23}
24
25impl Mind {
26 pub fn from(tasks: Vec<Task>, reminders: Vec<Reminder>) -> Self {
27 Self {
28 tasks,
29 reminders,
30 focused: None,
31 }
32 }
33
34 fn push(&mut self, task: Task) {
35 if let Some((_task, idx)) = self
36 .tasks
37 .iter()
38 .zip(0..)
39 .find(|(t, _i)| t.name().trim() == task.name().trim())
40 {
41 let task = self.tasks.remove(idx);
42 self.tasks.push(task);
43 } else {
44 self.tasks.push(task);
45 }
46 }
47
48 fn pop(&mut self) -> Option<Task> {
49 self.tasks.pop()
50 }
51
52 pub fn version() -> &'static str {
54 VERSION
55 }
56
57 pub fn tasks(&self) -> &Vec<Task> {
59 &self.tasks
60 }
61
62 pub fn reminders(&self) -> &Vec<Reminder> {
64 &self.reminders
65 }
66
67 pub fn focused(&self) -> Option<&Task> {
69 self.focused
70 .map(|idx| self.tasks.get(idx).map(Some).unwrap_or(None))
71 .unwrap_or(None)
72 }
73
74 pub fn remind_tasks(&mut self) {
76 let now = Local::now();
77 let mut new_reminders: Vec<Reminder> = Vec::new();
78
79 for reminder in self.reminders.clone() {
80 if reminder.when() > &now {
81 new_reminders.push(reminder);
82 continue;
83 }
84
85 self.push(Task::from_reminder(&reminder));
86
87 if let Some(upcoming) = reminder.upcoming(Some(now)) {
88 new_reminders.push(upcoming);
89 }
90 }
91 self.reminders = new_reminders;
92 }
93
94 pub fn backlog(&self) -> Duration {
96 let now = Local::now();
97 self.tasks
98 .iter()
99 .map(|t| (now - *t.start()))
100 .fold(Duration::zero(), |x, y| x + y)
101 }
102
103 pub fn productivity(&self) -> Productivity {
105 Productivity::from_backlog(self.backlog())
106 }
107
108 fn edit(&mut self, index: usize) -> io::Result<()> {
109 let task = self.tasks.get_mut(index).expect("invalid index");
110 let path = env::temp_dir().join("___mind___tmp_task___.md");
111
112 {
113 let mut file = fs::File::create(&path)?;
114 write!(file, "{}", task)?;
115 }
116
117 process::Command::new(env::var("EDITOR").unwrap_or_else(|_| "vi".into()))
118 .arg(&path)
119 .status()
120 .expect("failed to open editor");
121
122 let mut contents = String::new();
123 fs::File::open(&path)?.read_to_string(&mut contents)?;
124 let mut lines = contents.lines();
125 let name = lines.next().expect("missing the task name");
126 lines.next();
127
128 let details = lines.collect::<Vec<&str>>().join("\n");
129 let details = details.trim();
130
131 task.edit(
132 name.trim_start_matches("# ").trim().into(),
133 if details.chars().count() > 0 {
134 Some(details.into())
135 } else {
136 None
137 },
138 );
139
140 fs::remove_file(path)
141 }
142
143 fn edit_reminders(&mut self) -> io::Result<()> {
144 let reminders = self.reminders();
147 let lines: Vec<String> = serde_yaml::to_string(reminders)
148 .expect("failed to encode reminders")
149 .lines()
150 .map(String::from)
151 .chain(["#", "# # Examples"].iter().map(|l| l.to_string()))
152 .chain(Reminder::examples().lines().map(|l| format!("# {}", l)))
153 .collect();
154
155 let path = env::temp_dir().join("___mind___tmp_reminders___.yml");
156 {
157 let mut file = fs::File::create(&path)?;
158 write!(file, "{}", lines.join("\n"))?;
159 }
160
161 loop {
162 process::Command::new(env::var("EDITOR").unwrap_or_else(|_| "vi".into()))
163 .arg(&path)
164 .status()
165 .expect("failed to open editor");
166
167 let mut file = File::open(&path)?;
168 let mut content = String::new();
169 file.read_to_string(&mut content)?;
170
171 if content.is_empty() {
172 break;
173 }
174
175 let probably_reminders = serde_yaml::from_str(content.trim());
176 match probably_reminders {
177 Ok(reminders) => {
178 self.reminders = reminders;
179 break;
180 }
181
182 Err(err) => {
183 let updated_lines: Vec<String> = [
184 "# There was an error in the previous attempt",
185 "# ┌─────────────────────────────────────────",
186 format!("{}", err)
187 .lines()
188 .map(|l| format!("# │ ERROR: {}", &l))
189 .collect::<Vec<String>>()
190 .join("\n")
191 .trim(),
192 "# └────────────────────────────────────────────────────────",
193 "# If you want to cancel or quit, just leave this file empty",
194 ]
195 .iter()
196 .map(|l| l.to_string())
197 .chain(
198 content
199 .lines()
200 .map(String::from)
201 .skip_while(|l| l.starts_with("# ")),
202 )
203 .collect();
204
205 {
206 let mut file = fs::File::create(&path)?;
207 write!(file, "{}", updated_lines.join("\n"))?;
208 }
209 }
210 };
211 }
212
213 fs::remove_file(path)
214 }
215
216 pub fn task_to_reminder(&mut self, index: usize) -> io::Result<()> {
218 let task = self.tasks.remove(index);
219 let reminder = Reminder::new(
220 task.name().clone(),
221 task.details().clone(),
222 Local::now(),
223 Repeat::Never,
224 );
225 self.reminders.insert(0, reminder);
226 self.edit_reminders()
227 }
228
229 pub fn act(&mut self, command: Command) {
231 self.focused = None;
232
233 match command {
234 Command::Push(name) => {
235 self.push(Task::new(name));
236 }
237
238 Command::Continue(index) => {
239 if index < self.tasks.len() {
240 let task = self.tasks.remove(index);
241 self.tasks.push(task);
242 }
243 }
244
245 Command::Get(index) => {
246 if index < self.tasks.len() {
247 self.focused = Some(index);
248 }
249 }
250
251 Command::GetLast => {
252 if !self.tasks.is_empty() {
253 self.focused = Some(self.tasks.len() - 1);
254 }
255 }
256
257 Command::Pop(index) => {
258 if index < self.tasks.len() {
259 self.tasks.remove(index);
260 }
261 }
262
263 Command::PopLast => {
264 self.pop();
265 }
266
267 Command::Edit(index) => {
268 if index < self.tasks.len() {
269 self.edit(index).expect("failed to edit");
270 }
271 }
272
273 Command::EditLast => {
274 if !self.tasks.is_empty() {
275 self.edit(self.tasks.len() - 1).expect("failed to edit");
276 }
277 }
278 Command::Remind(index) => {
279 if index < self.tasks.len() {
280 self.task_to_reminder(index).expect("failed to edit");
281 }
282 }
283
284 Command::RemindLast => {
285 if !self.tasks.is_empty() {
286 self.task_to_reminder(self.tasks.len() - 1)
287 .expect("failed to edit");
288 }
289 }
290
291 Command::EditReminders => self.edit_reminders().expect("failed to edit"),
292 }
293 }
294}
295
296impl fmt::Display for Mind {
297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298 let mut color = 155u8;
299 let len = self.tasks.len();
300 let max_name_width = terminal_size().unwrap_or((100, 0)).0 as usize - 30;
301
302 let name_width = self
303 .tasks
304 .iter()
305 .map(|t| t.name().chars().count().min(max_name_width))
306 .max()
307 .unwrap_or(0);
308 let idx_width = len.to_string().chars().count();
309
310 let now = Local::now();
311
312 for (task, idx) in self.tasks.iter().zip(0..) {
313 let name = task.name().chars().take(max_name_width);
314
315 let name_color = match self.productivity() {
316 Productivity::Optimal => color::Fg(color::Rgb(0, color, 0)),
317 Productivity::High => color::Fg(color::Rgb(color - 75, color - 25, 0)),
318 Productivity::Normal => color::Fg(color::Rgb(color - 50, color - 50, 0)),
319 Productivity::Low => color::Fg(color::Rgb(color - 25, color - 75, 0)),
320 Productivity::UnProductive => color::Fg(color::Rgb(color, 0, 0)),
321 };
322
323 if atty::is(atty::Stream::Stdout) {
324 write!(
325 f,
326 "[{idx:idx_width$}] {name_color}{name:name_width$}\t{age_color}{age}{reset_color}",
327 idx = idx,
328 idx_width = idx_width,
329 name_color = name_color,
330 name = name.collect::<String>(),
331 age_color = color::Fg(color::Rgb(color - 50, color - 50, color - 50)),
332 age = &HumanTime::from(*task.start() - now),
333 reset_color = color::Fg(color::Reset),
334 name_width = name_width
335 )?;
336 } else {
337 write!(
338 f,
339 "[{idx:idx_width$}] {name:width$}\t{age}",
340 idx = idx,
341 idx_width = idx_width,
342 name = name.collect::<String>(),
343 age = &HumanTime::from(*task.start() - now),
344 width = name_width
345 )?;
346 }
347
348 if let Some(focused) = self.focused {
349 if focused == idx {
350 writeln!(f)?;
351 write!(f, "{}", &task)?;
352 }
353 }
354
355 if idx < len - 1 {
356 writeln!(f)?
357 }
358
359 color += 100u8 / len as u8;
360 }
361 Ok(())
362 }
363}