1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct HematiteTask {
6 pub id: usize,
7 pub text: String,
8 pub done: bool,
9}
10
11fn store_path() -> PathBuf {
12 crate::tools::file_ops::hematite_dir().join("tasks.json")
13}
14
15pub fn load() -> Vec<HematiteTask> {
16 let path = store_path();
17 let Ok(raw) = std::fs::read_to_string(&path) else {
18 return Vec::new();
19 };
20 serde_json::from_str(&raw).unwrap_or_default()
21}
22
23fn save(tasks: &[HematiteTask]) {
24 let path = store_path();
25 if let Some(parent) = path.parent() {
26 let _ = std::fs::create_dir_all(parent);
27 }
28 if let Ok(json) = serde_json::to_string_pretty(tasks) {
29 let _ = std::fs::write(&path, json);
30 }
31}
32
33pub fn add(text: &str) -> Vec<HematiteTask> {
34 let mut tasks = load();
35 let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1;
36 tasks.push(HematiteTask {
37 id: next_id,
38 text: text.trim().to_string(),
39 done: false,
40 });
41 save(&tasks);
42 tasks
43}
44
45pub fn mark_done(n: usize) -> Result<Vec<HematiteTask>, String> {
46 let mut tasks = load();
47 let task = tasks
48 .iter_mut()
49 .find(|t| t.id == n)
50 .ok_or_else(|| format!("No task with id {}.", n))?;
51 task.done = true;
52 save(&tasks);
53 Ok(tasks)
54}
55
56pub fn remove(n: usize) -> Result<Vec<HematiteTask>, String> {
57 let mut tasks = load();
58 let before = tasks.len();
59 tasks.retain(|t| t.id != n);
60 if tasks.len() == before {
61 return Err(format!("No task with id {}.", n));
62 }
63 save(&tasks);
64 Ok(tasks)
65}
66
67pub fn clear() {
68 save(&[]);
69}
70
71pub fn render_prompt_block(tasks: &[HematiteTask]) -> Option<String> {
73 if tasks.is_empty() {
74 return None;
75 }
76 let mut out = String::from("## Active Task List\n");
77 for t in tasks {
78 let mark = if t.done { "[x]" } else { "[ ]" };
79 out.push_str(&format!("{} {}. {}\n", mark, t.id, t.text));
80 }
81 out.push_str(
82 "\nWhen you complete a task, let the user know and suggest running `/task done <N>`.",
83 );
84 Some(out)
85}
86
87pub fn render_list(tasks: &[HematiteTask]) -> String {
89 if tasks.is_empty() {
90 return "No tasks. Use `/task add <text>` to add one.".to_string();
91 }
92 let mut out = String::from("## Task List\n\n");
93 for t in tasks {
94 let mark = if t.done { "[x]" } else { "[ ]" };
95 out.push_str(&format!("{} **{}**. {}\n", mark, t.id, t.text));
96 }
97 out
98}