todo/
lib.rs

1use ansi_term::Colour::{Blue, Cyan, Green, Purple, Red, Yellow};
2use ansi_term::Style;
3use regex::Regex;
4use std::fs::{File, OpenOptions};
5use std::io::{self, BufRead, BufReader, BufWriter, Write};
6use std::path::Path;
7use std::path::PathBuf;
8use std::process::exit;
9use xdg::BaseDirectories;
10
11// TODO: Add backup option (make use of todo/config.ini)
12pub struct ToDo {
13    pub todo_path: PathBuf,
14    pub config_path: PathBuf,
15}
16
17// TODO: make add to accept line-type tasks instead of one word
18// TODO: Add date and time
19// TODO: Add sort via date-time
20// TODO: make use of macros for open file with read or write permissions
21
22impl ToDo {
23    // TODO: make todo/config.ini usable
24    // TODO: Add config options to todo/config
25    pub fn new() -> Result<Self, String> {
26        let xdg_dir = BaseDirectories::with_prefix("ToDo").expect("Failed to get XDG directories.");
27
28        let config_path = xdg_dir
29            .place_config_file("config.ini")
30            .expect("Unable to create Config file.");
31
32        // TODO: create a separate function to do this work
33        if !Path::new(&config_path.as_path()).exists() {
34            File::create(&config_path).expect("Failed to create Config file.");
35        }
36
37        let todo_path = xdg_dir
38            .place_config_file("todo.lst")
39            .expect("Unable to create ToDo lst file.");
40
41        // TODO: create a separate function to do this work
42        if !Path::new(&todo_path.as_path()).exists() {
43            File::create(&todo_path).expect("Failed to create ToDo lst file.");
44        }
45
46        Ok(Self {
47            todo_path,
48            config_path,
49        })
50    }
51
52    // Add new task in todo
53    pub fn add(&self, args: &[String]) {
54        if args.is_empty() {
55            eprintln!("Add option needs atleast 1 argument.");
56            exit(1);
57        }
58
59        // Write contents in todo.lst
60        let todo_file = OpenOptions::new()
61            .create(true)
62            .read(true)
63            .append(true)
64            .open(&self.todo_path)
65            .expect("Unable to open todo.lst.");
66        let mut buffer_writter = BufWriter::new(&todo_file);
67
68        for arg in args {
69            if arg.trim().is_empty() {
70                continue;
71            }
72
73            // Remove one or more spaces and trim task
74            let re_multiple_spaces = Regex::new(r"\s+").unwrap();
75            let formated_task: String =
76                re_multiple_spaces.replace_all(&arg.trim(), "_").to_string();
77            // Add \n to every task
78            let line: String = format!("{} 0\n", &formated_task);
79
80            buffer_writter
81                .write_all(&line.as_bytes())
82                .expect("Unable to write task in todo.lst.");
83
84            println!("{}: {}", Purple.bold().paint("Added"), arg.trim());
85        }
86    }
87
88    // List all tasks in todo
89    pub fn list(&self, via_status: Option<u8>) {
90        // Open todo.lst to read
91        // BUG: OpenOptions::new() not working here
92        let todo_file = File::open(&self.todo_path).expect("Unable to open todo.lst.");
93        // Read buffer
94        let buffer_reader = BufReader::new(&todo_file);
95
96        let mut index = 1;
97        for line in buffer_reader.lines() {
98            if let Ok(line) = line {
99                let task_details: Vec<&str> = line.split_whitespace().collect();
100                // TODO: make function return Result<boolean> and make task_status boolean
101                let task_status: u8 = task_details[1].parse::<u8>().unwrap_or(0);
102
103                match via_status {
104                    Some(via) => {
105                        if via == 1 && task_status == 1 {
106                            println!(
107                                "{}: {}",
108                                Yellow.bold().italic().paint(&index.to_string()),
109                                Style::new().paint(task_details[0])
110                            );
111                        } else if via == 0 && task_status == 0 {
112                            println!(
113                                "{}: {}",
114                                Blue.bold().italic().paint(&index.to_string()),
115                                Style::new().paint(task_details[0])
116                            );
117                        }
118                    }
119                    None => {
120                        if task_status == 1 {
121                            println!(
122                                "{}: {} {}",
123                                Yellow.bold().italic().paint(&index.to_string()),
124                                Style::new().strikethrough().italic().paint(task_details[0]),
125                                Green.bold().paint("")
126                            );
127                        } else {
128                            println!(
129                                "{}: {} {}",
130                                Blue.bold().italic().paint(&index.to_string()),
131                                &task_details[0].to_string().replace("_", " "),
132                                Cyan.bold().paint("󰚭")
133                            );
134                        }
135                    }
136                }
137                index += 1;
138            }
139        }
140    }
141
142    // Completed a task from todo.lst
143    // NOTE: 1 - done
144    // NOTE: 0 - undone
145    pub fn done_undone(&self, args: &[String], status_todo: u8) {
146        if args.is_empty() {
147            eprintln!("done option needs atleast 1 argument.");
148            exit(1);
149        }
150        let done_line_no: Vec<u64> = args[..].iter().map(|z| z.parse::<u64>().unwrap()).collect();
151
152        // Open todo.lst to read
153        // BUG: OpenOptions::new() not working here
154        let mut todo_file = File::open(&self.todo_path).expect("Unable to open todo.lst.");
155        // Read Buffer
156        let buffer_reader = BufReader::new(&todo_file);
157
158        let mut new_list: Vec<String> = Vec::new();
159
160        let mut index: u64 = 1;
161        for line in buffer_reader.lines() {
162            let line = line.unwrap();
163            if done_line_no.contains(&index) {
164                let mut task_details: Vec<&str> = line.split_whitespace().collect();
165
166                // Update the taak status
167                task_details[1] = if status_todo == 1 { "1" } else { "0" };
168
169                let updated_line: String = format!(
170                    "{} {}",
171                    task_details[0].to_string(),
172                    task_details[1].to_string()
173                );
174
175                match status_todo {
176                    1 => println!(
177                        "{}: {}  : {}",
178                        Purple.bold().italic().paint(&index.to_string()),
179                        &task_details[0],
180                        Green.bold().paint("Completed ")
181                    ),
182                    0 => println!(
183                        "{}: {}  : {}",
184                        Purple.bold().italic().paint(&index.to_string()),
185                        &task_details[0],
186                        Cyan.bold().paint("UnDone 󰚭")
187                    ),
188                    _ => println!("{}", Red.paint("Configuration is wrong.")),
189                }
190
191                new_list.push(updated_line);
192            } else {
193                new_list.push(line);
194            }
195            index += 1;
196        }
197
198        // rewritting new_list to todo.lst
199        todo_file = OpenOptions::new()
200            .read(true)
201            .write(true)
202            .create(true)
203            .truncate(true)
204            .open(&self.todo_path)
205            .expect("Unable to open todo.lst.");
206        // Write Buffer
207        let mut buffer_writter = BufWriter::new(&todo_file);
208        for line in new_list {
209            // Add \n to every task
210            let line: String = format!("{}\n", line);
211            buffer_writter
212                .write_all(&line.as_bytes())
213                .expect("Unable to write to todo.lst.");
214        }
215    }
216
217    // Remove a task from todo.lst
218    pub fn rm(&self, args: &[String]) {
219        if args.is_empty() {
220            eprintln!("rm option needs atleast 1 argument.");
221            exit(1);
222        }
223
224        let mut del_line_no: Vec<u64> =
225            args[..].iter().map(|z| z.parse::<u64>().unwrap()).collect();
226        del_line_no.sort();
227
228        // Open todo.lst to read
229        // BUG: OpenOptions::new() not working here
230        let mut todo_file = File::open(&self.todo_path).expect("Unable to open todo.lst.");
231        // Read Buffer
232        let buffer_reader = BufReader::new(&todo_file);
233
234        let mut new_list: Vec<String> = vec![];
235        let mut index: u64 = 1;
236        for line in buffer_reader.lines() {
237            let task_details = line.unwrap_or("".to_string());
238            if !del_line_no.contains(&index) {
239                new_list.push(task_details);
240            } else {
241                let task = task_details.split_whitespace().nth(0).to_owned();
242                println!(
243                    "{} {}: {}",
244                    Red.bold().paint("Removed"),
245                    Purple.bold().italic().paint(&index.to_string()),
246                    &task.unwrap_or("")
247                );
248            }
249            index += 1;
250        }
251
252        // rewritting new_list to todo.lst
253        todo_file = OpenOptions::new()
254            .read(true)
255            .write(true)
256            .create(true)
257            .truncate(true)
258            .open(&self.todo_path)
259            .expect("Unable to open todo.lst.");
260        // Write Buffer
261        let mut buffer_writter = BufWriter::new(&todo_file);
262        for line in new_list {
263            // Add \n to every task
264            let line: String = format!("{}\n", line);
265            buffer_writter
266                .write_all(&line.as_bytes())
267                .expect("Unable to write to todo.lst.");
268        }
269    }
270
271    // Remove all tasks from todo.lst
272    pub fn rm_all(&self) {
273        let mut confirmation = String::new();
274        println!(
275            "{}: Do you want to remove all tasks from todo ? {}",
276            Red.bold().paint("WARNING"),
277            Blue.bold().paint("(y/Y/yes/Yes/YES)")
278        );
279        io::stdin()
280            .read_line(&mut confirmation)
281            .expect("Unable to take confirmation.");
282
283        let confirm: Vec<String> = vec!["y".to_string(), "yes".to_string()];
284
285        // Convert input to lowercase and remove whitespace
286        let confirmation = confirmation.trim().to_lowercase();
287
288        if confirm.iter().any(|z| z == &confirmation) {
289            let todo_file = OpenOptions::new()
290                .write(true)
291                .truncate(true)
292                .create(true)
293                .open(&self.todo_path)
294                .expect("Unable to open todo.lst.");
295
296            let mut buffer_writer = BufWriter::new(&todo_file);
297            buffer_writer
298                .write_all(&"".as_bytes())
299                .expect("Unable to write to todo.lst.");
300            println!(
301                "{}",
302                Cyan.bold().paint("All task are removed from todo.lst.")
303            );
304        } else {
305            exit(1);
306        }
307    }
308
309    // sort all task asc
310    // NOTE: via 0 - asc
311    // NOTE: via 1 - dsc
312    // NOTE: via_status 0 - undone
313    // NOTE: via_status 1 - done
314    pub fn sort(&self, via: u8, via_status: Option<u8>) {
315        let todo_file = File::open(&self.todo_path).expect("Unable to open todo.lst.");
316
317        // Read Buffer
318        let buffer_reader = BufReader::new(&todo_file);
319        let mut todo_lst: Vec<String> = vec![];
320
321        let mut index = 1;
322        for line in buffer_reader.lines() {
323            if line.is_ok() {
324                let formated_line = format!("{} {}", index, line.unwrap());
325                todo_lst.push(formated_line);
326            }
327            index += 1;
328        }
329        // sort todo_lst
330        match via {
331            0u8 => {
332                sort_by_key(&mut todo_lst, 1);
333                display_sorted(&todo_lst, via_status);
334            }
335            1u8 => {
336                sort_by_key(&mut todo_lst, 1);
337                todo_lst.reverse();
338                display_sorted(&todo_lst, via_status);
339            }
340            _ => println!("Configuration is incorrect."),
341        }
342    }
343
344    pub fn help(&self) {
345        println!("{}", USAGE_HELP);
346    }
347}
348
349// Helper functions
350
351// to display sorted List
352// NOTE: via_status means done or undone | Some(0) - undone, Some(1) - done
353fn display_sorted(todo_lst: &Vec<String>, via_status: Option<u8>) -> () {
354    for task in todo_lst {
355        // TODO: make function return Result<boolean> and make task_status boolean
356        let task_details: Vec<&str> = task.split_whitespace().collect();
357        let task_status: u8 = task_details[2].parse::<u8>().unwrap_or(0);
358        match via_status {
359            Some(via) => {
360                if via == 1 && task_status == 1 {
361                    println!(
362                        "{}: {}",
363                        Yellow.bold().italic().paint(task_details[0]),
364                        Style::new().paint(task_details[1])
365                    );
366                } else if via == 0 && task_status == 0 {
367                    println!(
368                        "{}: {}",
369                        Blue.bold().italic().paint(task_details[0]),
370                        Style::new().paint(task_details[1])
371                    );
372                }
373            }
374            None => {
375                if task_status == 1 {
376                    println!(
377                        "{}: {} {}",
378                        Yellow.bold().italic().paint(task_details[0]),
379                        Style::new().strikethrough().italic().paint(task_details[1]),
380                        Green.bold().paint("")
381                    );
382                } else {
383                    println!(
384                        "{}: {} {}",
385                        Blue.bold().italic().paint(task_details[0]),
386                        &task_details[1].to_string().replace("_", " "),
387                        Cyan.bold().paint("󰚭")
388                    );
389                }
390            }
391        }
392    }
393}
394
395// to sort by specific key
396fn sort_by_key(todo_lst: &mut Vec<String>, key: usize) -> () {
397    todo_lst.sort_by_key(|line| line.split_whitespace().nth(key).unwrap_or("").to_owned());
398}
399
400// Help Usage
401const USAGE_HELP: &str = "Usage: todo [OPTIONS] [ARGUMENTS]
402Todo is a blazingly fast CLI program written in Rust.
403
404    - add [TASK/s]: Adds new task/s.
405        Example: todo add 'do at least 10 dynamic programming questions.'
406        Example: todo add task1 task2 task3
407        Example: todo add 'mine task1', 'mine task2', 'mine task3'
408    
409    - list | list-all: Lists all tasks.
410        Example: todo list
411        Example: todo list-all
412    
413    - list-done: Lists all completed tasks.
414        Example: todo list-done
415    
416    - list-undone: Lists all pending tasks.
417        Example: todo list-undone
418    
419    - done [INDEX KEY]: Marks task as completed.
420        Example: todo done 5 6
421    
422    - undone [INDEX KEY]: Marks task as pending.
423        Example: todo undone 5 6
424    
425    - rm [INDEX KEY]: Removes a task.
426        Example: todo rm 2 1 3
427    
428    - rm-all | reset: Removes all tasks.
429        Example: todo rm-all
430        Example: todo reset
431    
432    - sort: Sorts all tasks (default - ascending order).
433        Example: todo sort
434    
435    - sort-asc: Sorts all tasks in ascending order.
436        Example: todo sort-asc
437    
438    - sort-dsc: Sorts all tasks in descending order.
439        Example: todo sort-dsc
440    
441    - sort-done: Sorts all completed tasks (default - ascending order).
442        Example: todo sort-done
443    
444    - sort-done-asc: Sorts all completed tasks in ascending order.
445        Example: todo sort-done-asc
446    
447    - sort-done-dsc: Sorts all completed tasks in descending order.
448        Example: todo sort-done-dsc
449    
450    - sort-undone: Sorts all pending tasks (default - ascending order).
451        Example: todo sort-undone
452    
453    - sort-undone-asc: Sorts all pending tasks in ascending order.
454        Example: todo sort-undone-asc
455    
456    - sort-undone-dsc: Sorts all pending tasks in descending order.
457        Example: todo sort-undone-dsc
458    
459Report any bugs or issues at: https://github.com/falcon71181/ToDo-rust";