kanbrain_core/api/
context.rs1use super::text::{head, 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 out.push_str("- ");
68 head(out, task)?;
69 if let Some(running) = task.running_for(now) {
70 write!(out, " · running {}", fmt_clock(running))?;
71 }
72 if let Some(note) = state_note(task) {
73 write!(out, " · {note}")?;
74 }
75 out.push('\n');
76 match task.asked() {
77 Some(Asked::Waiting(question)) => excerpt(out, "question", question, now)?,
78 Some(Asked::Answered(answer)) => excerpt(out, "answer", answer, now)?,
79 None if matches!(task.status, Status::Doing | Status::Parked) && task.is_task() => {
80 if let Some(comment) = task.comments.last() {
81 excerpt(out, "last comment", comment, now)?;
82 }
83 }
84 None => {}
85 }
86 }
87 if let Some(more) = tasks.len().checked_sub(limit).filter(|&n| n > 0) {
88 writeln!(out, "- … {more} more: `kb search` finds them")?;
89 }
90 out.push('\n');
91 Ok(())
92}
93
94fn excerpt(out: &mut String, what: &str, comment: &Comment, now: chrono::DateTime<chrono::Local>) -> std::fmt::Result {
96 let first = comment.body.lines().map(str::trim).find(|l| !l.is_empty()).unwrap_or_default();
97 let mut excerpt: String = first.chars().take(EXCERPT_CHARS).collect();
98 if excerpt.len() < first.len() || comment.body.trim() != first {
99 excerpt.push('…');
100 }
101 writeln!(out, " {what} ({} ago): {excerpt}", fmt_age(now - comment.created))
102}
103
104fn write_project(out: &mut String, project: &ProjectInfo) -> std::fmt::Result {
108 writeln!(out, "## Project {}\n", project.name)?;
109 if project.description.is_empty() {
112 writeln!(
113 out,
114 concat!(
115 "Not described yet. Before other work, describe the application from the README and manifests: \
116 `kb project describe -` (",
117 crate::projects::description_scope!(),
118 ").\n"
119 )
120 )?;
121 } else {
122 writeln!(out, "{}\n", project.description)?;
123 }
124 if project.siblings.is_empty() {
125 if !project.notes.is_empty() {
126 writeln!(out, "{}\n", project.notes)?;
127 }
128 return Ok(());
129 }
130 writeln!(out, "This is its `{}` repository.", project.repo)?;
131 if project.notes.is_empty() {
134 writeln!(out, "\nIts role isn't described yet: `kb project notes -` (what it owns, exposes and consumes).")?;
135 } else {
136 writeln!(out, "\n{}", project.notes)?;
137 }
138 writeln!(
139 out,
140 "\nIts other repositories. Work one of them needs, or a problem found in it, is a task there, not a note \
141 here: `kb --project {0}/NAME add TITLE`, then `kb link ID NAME#ID` relates it to the task here. \
142 `kb --project {0}/NAME search WORDS` finds its decisions and notes.\n",
143 project.name
144 )?;
145 for (name, dir) in &project.siblings {
146 let root = dir.parent().unwrap_or(dir);
147 writeln!(out, "- {name}: {}", root.display())?;
148 }
149 out.push('\n');
150 Ok(())
151}