1use super::text::state_note;
6use crate::brain::Brain;
7use crate::model::{Asked, Comment, Kind, Status, Task, fmt_age, fmt_clock, now};
8use crate::projects::ProjectInfo;
9use std::fmt::Write;
10
11const SECTION_LIMIT: usize = 20;
13const DONE_LIMIT: usize = 5;
15const EXCERPT_CHARS: usize = 120;
17
18impl Brain {
19 pub fn context_markdown(&self) -> String {
22 let mut out = String::new();
23 self.write_context(&mut out).expect("writing to a String cannot fail");
24 out
25 }
26
27 fn write_context(&self, out: &mut String) -> std::fmt::Result {
29 let scope = self.store.project().map_or_else(|| "central brain".to_string(), |p| format!("project {p}"));
30 writeln!(out, "# kanbrain · {scope}\n")?;
31 if let Some(project) = &self.project {
32 write_project(out, project)?;
33 }
34 let answered = self.newest(|t| matches!(t.asked(), Some(Asked::Answered(_))));
36 section(out, "Answered: pick these up first", &answered, SECTION_LIMIT)?;
37 for status in [Status::Doing, Status::Parked, Status::Todo] {
38 let tasks =
39 self.newest(|t| t.is_task() && t.status == status && !matches!(t.asked(), Some(Asked::Answered(_))));
40 section(out, &format!("Tasks: {}", status.as_str()), &tasks, SECTION_LIMIT)?;
41 }
42 let mut done = self.newest(|t| t.is_task() && t.status == Status::Done);
43 done.sort_by_key(|t| std::cmp::Reverse(t.status_since()));
44 section(out, "Tasks: done lately", &done, DONE_LIMIT)?;
45 section(out, "Decisions", &self.newest(|t| t.kind == Kind::Decision), SECTION_LIMIT)?;
46 section(out, "Notes", &self.newest(|t| t.kind == Kind::Note), SECTION_LIMIT)
47 }
48
49 fn newest(&self, keep: impl Fn(&Task) -> bool) -> Vec<&Task> {
51 let mut tasks: Vec<&Task> = self.tasks.iter().filter(|t| keep(t)).collect();
52 tasks.sort_by_key(|t| std::cmp::Reverse(t.id));
53 tasks
54 }
55}
56
57fn section(out: &mut String, title: &str, tasks: &[&Task], limit: usize) -> std::fmt::Result {
61 if tasks.is_empty() {
62 return Ok(());
63 }
64 writeln!(out, "## {title}\n")?;
65 let now = now();
66 for task in tasks.iter().take(limit) {
67 write!(out, "- #{} {}", task.id, task.title)?;
68 for tag in &task.tags {
69 write!(out, " #{tag}")?;
70 }
71 if let Some(running) = task.running_for(now) {
72 write!(out, " · running {}", fmt_clock(running))?;
73 }
74 if let Some(note) = state_note(task) {
75 write!(out, " · {note}")?;
76 }
77 out.push('\n');
78 match task.asked() {
79 Some(Asked::Waiting(question)) => excerpt(out, "question", question, now)?,
80 Some(Asked::Answered(answer)) => excerpt(out, "answer", answer, now)?,
81 None if matches!(task.status, Status::Doing | Status::Parked) && task.is_task() => {
82 if let Some(comment) = task.comments.last() {
83 excerpt(out, "last comment", comment, now)?;
84 }
85 }
86 None => {}
87 }
88 }
89 if let Some(more) = tasks.len().checked_sub(limit).filter(|&n| n > 0) {
90 writeln!(out, "- … {more} more: `kb search` finds them")?;
91 }
92 out.push('\n');
93 Ok(())
94}
95
96fn excerpt(out: &mut String, what: &str, comment: &Comment, now: chrono::DateTime<chrono::Local>) -> std::fmt::Result {
98 let first = comment.body.lines().map(str::trim).find(|l| !l.is_empty()).unwrap_or_default();
99 let mut excerpt: String = first.chars().take(EXCERPT_CHARS).collect();
100 if excerpt.len() < first.len() || comment.body.trim() != first {
101 excerpt.push('…');
102 }
103 writeln!(out, " {what} ({} ago): {excerpt}", fmt_age(now - comment.created))
104}
105
106fn write_project(out: &mut String, project: &ProjectInfo) -> std::fmt::Result {
110 writeln!(out, "## Project {}\n", project.name)?;
111 if project.description.is_empty() {
114 writeln!(
115 out,
116 concat!(
117 "Not described yet. Before other work, describe the application from the README and manifests: \
118 `kb project describe -` (",
119 crate::projects::description_scope!(),
120 ").\n"
121 )
122 )?;
123 } else {
124 writeln!(out, "{}\n", project.description)?;
125 }
126 if project.siblings.is_empty() {
127 if !project.notes.is_empty() {
128 writeln!(out, "{}\n", project.notes)?;
129 }
130 return Ok(());
131 }
132 writeln!(out, "This is its `{}` repository.", project.repo)?;
133 if project.notes.is_empty() {
136 writeln!(out, "\nIts role isn't described yet: `kb project notes -` (what it owns, exposes and consumes).")?;
137 } else {
138 writeln!(out, "\n{}", project.notes)?;
139 }
140 writeln!(
141 out,
142 "\nIts other repositories. Work one of them needs, or a problem found in it, is a task there, not a note \
143 here: `kb --project {0}/NAME add TITLE`, then `kb link ID NAME#ID` relates it to the task here. \
144 `kb --project {0}/NAME search WORDS` finds its decisions and notes.\n",
145 project.name
146 )?;
147 for (name, dir) in &project.siblings {
148 let root = dir.parent().unwrap_or(dir);
149 writeln!(out, "- {name}: {}", root.display())?;
150 }
151 out.push('\n');
152 Ok(())
153}