use std::fmt::Write;
use jiff::Timestamp;
use crate::cmd::age;
use crate::git::{Distance, Git};
use crate::graph;
use crate::model::Status;
use crate::store::Db;
const STALE_AFTER: u64 = 50;
const CONTRACT: &str = "\
foam tracks this repository's issues and memories on a git ref; nothing is \
checked out and nothing here is a file to edit. Start with `foam ready` and \
`foam claim <id>` before working on an issue. Record progress with \
`foam note <id> <text>`, finish with `foam close <id> --reason <why>`, and \
`foam unclaim <id>` anything you stop working on. Store facts the next \
session needs with `foam remember <slug> <text>`; check `foam memories` \
before relying on one that is marked as old or from another branch.";
const CHEAT_SHEET: &str = "\
foam ready [--limit N] issues that can be worked now
foam show <id> one issue in full, with notes and blockers
foam create <title> [--type T] [-p 0-4] [--blocked-by ID] [--parent ID]
foam claim <id> | unclaim <id> | heartbeat <id>
foam note <id> <text> append a note
foam close <id> --reason <why> | foam reopen <id>
foam dep add <id> <blocker> make <id> wait on <blocker>
foam blocked what is waiting, and on what
foam search <query> titles, bodies and notes
foam remember <slug> <text> | memories | recall <slug> | forget <slug>
foam update <id> [--title ..] [--priority N] [--defer-until DATE]
Add --json to any command for machine-readable output.";
pub fn render(db: &Db, git: &Git, actor: &str, limit: usize) -> String {
let now = Timestamp::now();
let mut out = String::new();
out.push_str("# foam\n\n");
out.push_str(CONTRACT);
out.push_str("\n\n## Commands\n\n");
out.push_str(CHEAT_SHEET);
out.push('\n');
let count = |s: Status| db.issues.values().filter(|i| i.status == s).count();
let stamp = git.head_stamp().ok();
let (branch, commit) = stamp
.as_ref()
.map(|s| (s.branch.as_str(), s.commit.as_str()))
.unwrap_or(("?", "?"));
let _ = write!(
out,
"\n## Status\n\n{} open, {} in progress, {} deferred, {} closed; on {branch} at {commit}\n",
count(Status::Open),
count(Status::InProgress),
count(Status::Deferred),
count(Status::Closed),
);
let expired = db
.issues
.values()
.filter(|i| i.status == Status::InProgress && i.lease_expires.is_none_or(|t| t <= now))
.count();
if expired > 0 {
let _ = writeln!(
out,
"{expired} in-progress issue(s) have an expired lease; `foam reclaim` reopens them"
);
}
let mine: Vec<_> = db
.issues
.values()
.filter(|i| i.status == Status::InProgress && i.assignee.as_deref() == Some(actor))
.collect();
if !mine.is_empty() {
let _ = write!(out, "\n## In progress for {actor}\n\n");
for i in mine {
let lease = match i.lease_expires {
Some(t) if t > now => {
let mins = t.duration_since(now).as_mins();
format!("lease {mins} min left")
}
_ => "lease expired".to_string(),
};
let _ = writeln!(out, "{} P{} {} ({lease})", i.id, i.priority, i.title);
}
}
let ready = graph::ready(db, now);
let _ = write!(out, "\n## Ready ({} total)\n\n", ready.len());
if ready.is_empty() {
out.push_str("nothing is ready\n");
}
for i in ready.iter().take(limit) {
let _ = writeln!(out, "{} P{} {} {}", i.id, i.priority, i.kind, i.title);
}
if !db.memories.is_empty() {
out.push_str("\n## Memories\n\n");
for m in db.memories.values() {
let distance = git.distance(&m.stamp.commit);
let note = match distance {
Distance::Behind(n) if n < STALE_AFTER => String::new(),
_ => format!(" [{}]", age(&m.stamp, distance)),
};
let _ = writeln!(out, "{}: {}{note}", m.slug, m.text);
}
}
out
}
pub fn hook_json(context: &str) -> String {
serde_json::json!({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": context,
}
})
.to_string()
}