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}