Skip to main content

hardly_working/
task.rs

1use crate::config::TodoListLocation;
2use colored::Colorize;
3use std::collections::HashMap;
4use std::fs::{self, File};
5use std::io::{self, Read, Write};
6use std::path::PathBuf;
7
8pub struct Task {
9    pub description: String,
10    pub completed: bool,
11    pub group: Option<String>,
12}
13
14pub struct TodoList {
15    pub tasks: Vec<Task>,
16}
17
18impl TodoList {
19    pub fn new() -> Self {
20        Self { tasks: Vec::new() }
21    }
22
23    pub fn add_task(&mut self, description: String, group: Option<String>) {
24        self.tasks.push(Task {
25            description,
26            completed: false,
27            group,
28        });
29    }
30
31    pub fn toggle_task(&mut self, task_id: usize) -> Result<(), String> {
32        if task_id == 0 || task_id > self.tasks.len() {
33            return Err(format!("Task {} does not exist", task_id));
34        }
35        let task = &mut self.tasks[task_id - 1];
36        task.completed = !task.completed;
37        Ok(())
38    }
39
40    pub fn remove_task(&mut self, task_id: usize) -> Result<(), String> {
41        if task_id == 0 || task_id > self.tasks.len() {
42            return Err(format!("Task {} does not exist", task_id));
43        }
44        self.tasks.remove(task_id - 1);
45        Ok(())
46    }
47
48    pub fn clear(&mut self) {
49        self.tasks.retain(|task| !task.completed);
50    }
51
52    fn display_task_aligned(&self, index: usize, task: &Task, width: usize) {
53        let line_num = format!("{:>width$}:", index + 1, width = width);
54        let checkbox = if task.completed {
55            "[x]".green().bold()
56        } else {
57            "[ ]".red()
58        };
59        let description = if task.completed {
60            task.description.green().strikethrough()
61        } else {
62            task.description.white()
63        };
64        println!("{} {} {}", line_num.blue().bold(), checkbox, description);
65    }
66
67    pub fn list_tasks(&self) {
68        if self.tasks.is_empty() {
69            println!(
70                "{}",
71                "You're Hardly Working! Work harder with 'hw add <task>'".yellow()
72            );
73            return;
74        }
75
76        let max_index_width = self.tasks.len().to_string().len();
77
78        let mut tasks_by_group: HashMap<Option<String>, Vec<(usize, &Task)>> = HashMap::new();
79        for (task_id, task) in self.tasks.iter().enumerate() {
80            tasks_by_group
81                .entry(task.group.clone())
82                .or_default()
83                .push((task_id, task));
84        }
85
86        if let Some(tasks) = tasks_by_group.get(&None) {
87            for &(i, task) in tasks {
88                self.display_task_aligned(i, task, max_index_width);
89            }
90        }
91
92        let mut groups: Vec<_> = tasks_by_group.iter().filter(|(k, _)| k.is_some()).collect();
93        groups.sort_by(|(a, _), (b, _)| a.cmp(b));
94
95        for (group, tasks) in groups {
96            if let Some(group_name) = group {
97                println!("\n{}", group_name.blue().bold().underline());
98                for &(i, task) in tasks {
99                    self.display_task_aligned(i, task, max_index_width);
100                }
101            }
102        }
103    }
104
105    pub fn edit_task(&mut self, task_id: usize, new_description: String) -> Result<(), String> {
106        if task_id == 0 || task_id > self.tasks.len() {
107            return Err(format!("Task {} does not exist", task_id));
108        }
109
110        let task = &mut self.tasks[task_id - 1];
111        task.description = new_description;
112
113        Ok(())
114    }
115
116    pub fn search_tasks(&self, partial_description: &str) -> Vec<(usize, &Task)> {
117        self.tasks
118            .iter()
119            .enumerate()
120            .filter(|(_, task)| {
121                task.description
122                    .to_lowercase()
123                    .contains(&partial_description.to_lowercase())
124            })
125            .collect()
126    }
127
128    pub fn display_task(&self, index: usize, task: &Task) {
129        let line_num = format!("{}:", index + 1);
130        let checkbox = if task.completed {
131            "[x]".green().bold()
132        } else {
133            "[ ]".red()
134        };
135        let description = if task.completed {
136            task.description.green().strikethrough()
137        } else {
138            task.description.white()
139        };
140        println!("{} {} {}", line_num.blue().bold(), checkbox, description);
141    }
142
143    pub fn to_markdown(&self) -> String {
144        let mut content = String::new();
145
146        content.push_str("# Hardly Working TODO List\n\n");
147
148        let mut tasks_by_group: HashMap<Option<String>, Vec<&Task>> = HashMap::new();
149        for task in &self.tasks {
150            tasks_by_group
151                .entry(task.group.clone())
152                .or_default()
153                .push(task);
154        }
155
156        if let Some(tasks) = tasks_by_group.get(&None) {
157            for task in tasks {
158                let checkbox = if task.completed { "[x]" } else { "[ ]" };
159                content.push_str(&format!("- {} {}\n", checkbox, task.description));
160            }
161            content.push('\n');
162        }
163
164        let mut groups: Vec<_> = tasks_by_group.iter().filter(|(k, _)| k.is_some()).collect();
165        groups.sort_by(|(a, _), (b, _)| a.cmp(b));
166
167        for (group, tasks) in groups {
168            if let Some(group_name) = group {
169                content.push_str(&format!("## {}\n\n", group_name));
170                for task in tasks {
171                    let checkbox = if task.completed { "[x]" } else { "[ ]" };
172                    content.push_str(&format!("- {} {}\n", checkbox, task.description));
173                }
174                content.push('\n');
175            }
176        }
177        content
178    }
179
180    pub fn from_markdown(content: &str) -> Self {
181        let mut tasks = Vec::new();
182        let mut current_group: Option<String> = None;
183
184        for line in content.lines() {
185            let line = line.trim();
186            if line.is_empty() {
187                continue;
188            }
189
190            if line.starts_with("# ") {
191                continue;
192            }
193
194            if line.starts_with("## ") {
195                current_group = Some(line[3..].trim().to_string());
196                continue;
197            }
198
199            if line.starts_with("- [") && line.len() > 6 {
200                let completed = &line[3..4] == "x";
201                let description = line[6..].trim().to_string();
202                tasks.push(Task {
203                    description,
204                    completed,
205                    group: current_group.clone(),
206                });
207            }
208        }
209
210        Self { tasks }
211    }
212
213    pub fn load(todo_list_location: &TodoListLocation) -> Self {
214        let path = PathBuf::from(&todo_list_location.file_path);
215        let mut file = match File::open(&path) {
216            Ok(file) => file,
217            Err(_) => return TodoList::new(),
218        };
219        let mut contents = String::new();
220        if file.read_to_string(&mut contents).is_err() {
221            return TodoList::new();
222        }
223        TodoList::from_markdown(&contents)
224    }
225
226    pub fn save(&self, todo_list_location: &TodoListLocation) -> io::Result<()> {
227        let path = PathBuf::from(&todo_list_location.file_path);
228        if let Some(parent_path) = path.parent() {
229            fs::create_dir_all(parent_path)?;
230        }
231        let content = self.to_markdown();
232        let mut file = File::create(path)?;
233        file.write_all(content.as_bytes())?;
234        Ok(())
235    }
236}