1use regex::Regex;
10
11pub fn update_last_updated(contents: &str, date: &str) -> String {
13 let re = Regex::new(r"(?m)^Last Updated: .+$").unwrap();
14 if re.is_match(contents) {
15 return re
16 .replace_all(contents, format!("Last Updated: {date}").as_str())
17 .to_string();
18 }
19 contents.to_string()
20}
21
22fn insert_after_heading(
23 contents: &str,
24 heading: &str,
25 line_to_insert: &str,
26) -> Result<String, String> {
27 let mut out: Vec<String> = Vec::new();
28 let mut inserted = false;
29 for line in contents.lines() {
30 out.push(line.to_string());
31 if !inserted && line.trim() == heading {
32 out.push(line_to_insert.to_string());
33 inserted = true;
34 }
35 }
36 if !inserted {
37 return Err(format!("Missing heading: {heading}"));
38 }
39 let mut s = out.join("\n");
40 s.push('\n');
41 Ok(s)
42}
43
44pub fn add_decision(contents: &str, date: &str, text: &str) -> Result<String, String> {
46 let line = format!("- {date}: {text}");
47 let updated = insert_after_heading(contents, "## Recent Decisions", &line)?;
48 Ok(update_last_updated(&updated, date))
49}
50
51pub fn add_question(contents: &str, date: &str, text: &str) -> Result<String, String> {
53 let line = format!("- [ ] {text}");
54 let updated = insert_after_heading(contents, "## Open Questions", &line)?;
55 Ok(update_last_updated(&updated, date))
56}
57
58pub fn add_blocker(contents: &str, date: &str, text: &str) -> Result<String, String> {
60 let mut lines: Vec<String> = contents.lines().map(|l| l.to_string()).collect();
61 let mut i = 0usize;
62 let mut in_blockers = false;
63 let mut inserted = false;
64 while i < lines.len() {
65 let line = lines[i].as_str();
66 if line.trim() == "## Blockers" {
67 in_blockers = true;
68 i += 1;
69 continue;
70 }
71 if in_blockers {
72 if line.starts_with("## ") {
73 lines.insert(i, format!("- {text}"));
74 inserted = true;
75 break;
76 }
77 if line.trim() == "[None currently]" {
78 lines[i] = format!("- {text}");
79 inserted = true;
80 break;
81 }
82 if line.trim().is_empty() {
83 lines.insert(i, format!("- {text}"));
84 inserted = true;
85 break;
86 }
87 }
88 i += 1;
89 }
90 if !inserted {
91 return Err("Could not find Blockers section".to_string());
92 }
93 let mut out = lines.join("\n");
94 out.push('\n');
95 Ok(update_last_updated(&out, date))
96}
97
98pub fn set_focus(contents: &str, date: &str, text: &str) -> Result<String, String> {
100 let re = Regex::new(r"(?m)(## Current Focus\n)([^\n#]*)").unwrap();
102 if !re.is_match(contents) {
103 return Err("Could not find Current Focus section".to_string());
104 }
105 let updated = re
106 .replace(contents, |caps: ®ex::Captures<'_>| {
107 format!("{}{}\n", &caps[1], text)
108 })
109 .to_string();
110 Ok(update_last_updated(&updated, date))
111}
112
113pub fn add_note(contents: &str, date: &str, time: &str, text: &str) -> Result<String, String> {
117 let session_header = format!("### {date} Session");
118 let entry = format!("- {time}: {text}");
119
120 let mut lines: Vec<String> = contents.lines().map(|l| l.to_string()).collect();
121
122 for i in 0..lines.len() {
124 if lines[i].trim() == session_header {
125 lines.insert(i + 1, entry);
126 let mut out = lines.join("\n");
127 out.push('\n');
128 return Ok(update_last_updated(&out, date));
129 }
130 }
131
132 for i in 0..lines.len() {
134 if lines[i].trim() == "## Session Notes" {
135 lines.insert(i + 1, session_header);
136 lines.insert(i + 2, entry);
137 let mut out = lines.join("\n");
138 out.push('\n');
139 return Ok(update_last_updated(&out, date));
140 }
141 }
142
143 Err("Could not find Session Notes section".to_string())
144}
145
146