use super::ApiError;
use crate::brain::Brain;
use crate::model::{Asked, Author, Comment, DecisionState, Task, fmt_age, fmt_dur, now};
use chrono::{DateTime, Local};
use std::fmt::Write;
const HISTORY_LIMIT: usize = 5;
pub const SEARCH_LIMIT: usize = 20;
pub fn line(task: &Task) -> String {
let mut state = if task.is_task() { task.status.as_str() } else { task.kind.as_str() }.to_string();
if let Some(also) = state_note(task) {
write!(state, ", {also}").expect("writing to a String cannot fail");
}
let mut line = format!("#{} {} ({state})", task.id, task.title);
for tag in &task.tags {
write!(line, " #{tag}").expect("writing to a String cannot fail");
}
line
}
pub fn state_note(task: &Task) -> Option<&'static str> {
match task.asked() {
Some(Asked::Waiting(_)) => return Some("waiting for an answer"),
Some(Asked::Answered(_)) => return Some("answered"),
None => {}
}
match task.decision {
DecisionState::Accepted => None,
state => Some(state.as_str()),
}
}
fn comment_heading(comment: &Comment) -> String {
let mut heading = when(comment.created);
if comment.author == Author::Agent {
heading.push_str(" · agent");
}
if comment.question {
heading.push_str(" · question");
}
if let Some(edited) = comment.edited {
write!(heading, " (edited {})", when(edited)).expect("writing to a String cannot fail");
}
heading
}
fn when(at: DateTime<Local>) -> String {
at.format("%Y-%m-%d %H:%M").to_string()
}
impl Brain {
pub fn entry_text(&self, id: u32) -> Result<String, ApiError> {
let task = self.entry(id)?;
let mut out = String::new();
self.write_entry(&mut out, task).expect("writing to a String cannot fail");
Ok(out)
}
pub fn search_text(&self, query: &str) -> String {
let found = self.search(query);
if found.is_empty() {
return "nothing found\n".to_string();
}
let mut out = String::new();
for task in found.iter().take(SEARCH_LIMIT) {
writeln!(out, "- {}", line(task)).expect("writing to a String cannot fail");
}
if let Some(more) = found.len().checked_sub(SEARCH_LIMIT).filter(|&n| n > 0) {
writeln!(out, "- … {more} more: add words to narrow it down").expect("writing to a String cannot fail");
}
out
}
fn write_entry(&self, out: &mut String, task: &Task) -> std::fmt::Result {
let now = now();
writeln!(out, "# {}", line(task))?;
let mut facts = vec![format!("created {}", when(task.created))];
if task.is_task() && !task.history.is_empty() {
facts.push(format!("{} for {}", task.status.as_str(), fmt_age(now - task.status_since())));
}
if !task.sessions.is_empty() {
let running = task.running_for(now).map(|r| format!(", running for {}", fmt_dur(r))).unwrap_or_default();
facts.push(format!("tracked {} in {} session(s){running}", fmt_dur(task.total(now)), task.sessions.len()));
}
writeln!(out, "{}", facts.join(" · "))?;
if let Some(path) = &task.path {
writeln!(out, "file: {}", path.display())?;
}
if !task.description.trim().is_empty() {
writeln!(out, "\n## Description\n\n{}", task.description.trim())?;
}
if !task.comments.is_empty() {
writeln!(out, "\n## Comments")?;
for comment in &task.comments {
writeln!(out, "\n### {}\n\n{}", comment_heading(comment), comment.body.trim())?;
}
}
let related = self.related(task.id);
let elsewhere = self.repo_related(task.id);
if !related.is_empty() || !elsewhere.is_empty() {
writeln!(out, "\n## Related\n")?;
for other in related {
writeln!(out, "- {}", line(other))?;
}
for (repo, other) in &elsewhere {
writeln!(out, "- {repo}{}", line(other))?;
}
}
if !task.history.is_empty() {
let skipped = task.history.len().saturating_sub(HISTORY_LIMIT);
let heading = if skipped > 0 {
format!("History (latest {HISTORY_LIMIT} of {})", task.history.len())
} else {
"History".to_string()
};
writeln!(out, "\n## {heading}\n")?;
for change in &task.history[skipped..] {
let by = if change.by == Author::Agent { " (agent)" } else { "" };
writeln!(out, "- {} {} -> {}{by}", when(change.at), change.from.as_str(), change.to.as_str())?;
}
}
Ok(())
}
}