ito_domain/tasks/
update.rs1use chrono::{DateTime, Local};
4use regex::Regex;
5
6use super::TaskStatus;
7
8use super::checkbox::split_checkbox_task_label;
9fn split_checkbox_line(t: &str) -> Option<(char, &str)> {
10 let bytes = t.as_bytes();
11 if bytes.len() < 5 {
12 return None;
13 }
14 let bullet = bytes[0] as char;
15 if bullet != '-' && bullet != '*' {
16 return None;
17 }
18 if bytes[1] != b' ' || bytes[2] != b'[' || bytes[4] != b']' {
19 return None;
20 }
21 Some((bullet, &t[5..]))
22}
23
24pub fn update_checkbox_task_status(
42 contents: &str,
43 task_id: &str,
44 new_status: TaskStatus,
45) -> Result<String, String> {
46 let new_marker = match new_status {
47 TaskStatus::Pending => ' ',
48 TaskStatus::InProgress => '~',
49 TaskStatus::Complete => 'x',
50 TaskStatus::Shelved => {
51 return Err("Checkbox-only tasks.md does not support shelving".into());
52 }
53 };
54
55 let mut lines: Vec<String> = Vec::new();
56 for line in contents.lines() {
57 lines.push(line.to_string());
58 }
59
60 for line in &mut lines {
62 let indent_len = line.len().saturating_sub(line.trim_start().len());
63 let indent = &line[..indent_len];
64 let t = &line[indent_len..];
65 let Some((bullet, after)) = split_checkbox_line(t) else {
66 continue;
67 };
68
69 let rest = after.trim_start();
70 let Some((id, _name)) = split_checkbox_task_label(rest) else {
71 continue;
72 };
73 if id != task_id {
74 continue;
75 }
76
77 *line = format!("{indent}{bullet} [{new_marker}]{after}");
78
79 let mut out = lines.join("\n");
80 out.push('\n');
81 return Ok(out);
82 }
83
84 let Ok(idx) = task_id.parse::<usize>() else {
85 return Err(format!("Task \"{task_id}\" not found"));
86 };
87 if idx == 0 {
88 return Err(format!("Task \"{task_id}\" not found"));
89 }
90
91 let mut count = 0usize;
92
93 for line in &mut lines {
94 let indent_len = line.len().saturating_sub(line.trim_start().len());
95 let indent = &line[..indent_len];
96 let t = &line[indent_len..];
97 let Some((bullet, after)) = split_checkbox_line(t) else {
98 continue;
99 };
100
101 count += 1;
102 if count != idx {
103 continue;
104 }
105
106 *line = format!("{indent}{bullet} [{new_marker}]{after}");
107 break;
108 }
109
110 if count < idx {
111 return Err(format!("Task \"{task_id}\" not found"));
112 }
113
114 let mut out = lines.join("\n");
115 out.push('\n');
116 Ok(out)
117}
118
119pub fn update_enhanced_task_status(
138 contents: &str,
139 task_id: &str,
140 new_status: TaskStatus,
141 now: DateTime<Local>,
142) -> String {
143 let heading = Regex::new(&format!(
145 r"(?m)^###\s+(?:Task\s+)?{}\s*:\s*.+$",
146 regex::escape(task_id)
147 ))
148 .unwrap();
149
150 let status_line = match new_status {
151 TaskStatus::Complete => "- **Status**: [x] complete".to_string(),
152 TaskStatus::InProgress => "- **Status**: [>] in-progress".to_string(),
153 TaskStatus::Pending => "- **Status**: [ ] pending".to_string(),
154 TaskStatus::Shelved => "- **Status**: [-] shelved".to_string(),
155 };
156
157 let date = now.format("%Y-%m-%d").to_string();
158 let updated_at_line = format!("- **Updated At**: {date}");
159
160 let mut lines: Vec<String> = Vec::new();
161 for line in contents.lines() {
162 lines.push(line.to_string());
163 }
164 let mut start_idx: Option<usize> = None;
165 for (i, line) in lines.iter().enumerate() {
166 if heading.is_match(line) {
167 start_idx = Some(i);
168 break;
169 }
170 }
171
172 if let Some(start) = start_idx {
173 let mut end = lines.len();
174 for (i, line) in lines.iter().enumerate().skip(start + 1) {
175 if line.starts_with("### ") || line.starts_with("## ") {
176 end = i;
177 break;
178 }
179 }
180
181 let mut status_idx: Option<usize> = None;
182 let mut updated_idx: Option<usize> = None;
183 for (i, line) in lines.iter().enumerate().take(end).skip(start + 1) {
184 let l = line.trim_start();
185 if status_idx.is_none() && l.starts_with("- **Status**:") {
186 status_idx = Some(i);
187 }
188 if updated_idx.is_none() && l.starts_with("- **Updated At**:") {
189 updated_idx = Some(i);
190 }
191 }
192
193 if let Some(i) = status_idx {
194 lines[i] = status_line.clone();
195 }
196 if let Some(i) = updated_idx {
197 lines[i] = updated_at_line.clone();
198 }
199
200 match (status_idx, updated_idx) {
201 (Some(s), None) => {
202 lines.insert(s, updated_at_line);
204 }
205 (None, Some(u)) => {
206 lines.insert(u + 1, status_line);
208 }
209 (None, None) => {
210 lines.insert(end, updated_at_line);
212 lines.insert(end + 1, status_line);
213 }
214 (Some(_status_idx), Some(_updated_idx)) => {}
215 }
216 }
217
218 let mut out = lines.join("\n");
220 out.push('\n');
221 out
222}