mind/
mind.rs

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
14// Access it using Mind::version()
15static VERSION: &str = env!("CARGO_PKG_VERSION");
16
17/// The productive mind.
18#[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    /// Get the version. See ~/.mind/version
53    pub fn version() -> &'static str {
54        VERSION
55    }
56
57    /// Get the pending tasks. See ~/.mind/tasks.yml
58    pub fn tasks(&self) -> &Vec<Task> {
59        &self.tasks
60    }
61
62    /// Get the reminders. See ~/.mind/reminders.yml
63    pub fn reminders(&self) -> &Vec<Reminder> {
64        &self.reminders
65    }
66
67    /// Get the focused task
68    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    /// Go through the reminders and taks proper action.
75    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    /// Total backlog
95    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    /// Productivity from backlog
104    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        // TODO: do the same for Task edit?
145
146        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    /// Turn the specified task into a reminder
217    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    /// Act based on the given command.
230    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}