Skip to main content

kranz_cli/
ticket_notes.rs

1//! The `kranz ticket note|notes` surface (D-BW-3, adopted from beads):
2//! environment concerns (note author) and terminal rendering over
3//! [`kranz_engine::ticket_notes`], which owns storage, the append-only
4//! durability idiom, and the draft-context folding — so every surface
5//! (CLI today, REST/Slack later) shares one implementation.
6
7use anyhow::{bail, Result};
8use kranz_engine::ticket::Ticket;
9use kranz_engine::ticket_notes::{self, TicketNote};
10use std::path::Path;
11
12/// Environment variable naming the author recorded on a new note.
13const AUTHOR_ENV: &str = "KRANZ_NOTE_AUTHOR";
14
15/// The author for a new note: `$KRANZ_NOTE_AUTHOR` when set and non-blank,
16/// else `"operator"`.
17pub fn note_author() -> String {
18    std::env::var(AUTHOR_ENV)
19        .ok()
20        .map(|a| a.trim().to_string())
21        .filter(|a| !a.is_empty())
22        .unwrap_or_else(|| "operator".to_string())
23}
24
25/// `kranz ticket note <slug> <text...>`: append one note to the ticket's
26/// discussion. Refuses on an unknown ticket (a typo'd slug must not orphan a
27/// notes sidecar with no ticket behind it).
28pub fn cmd_ticket_note(repo: &Path, slug: &str, text: &str) -> Result<String> {
29    let path = Ticket::tickets_dir(repo).join(format!("{slug}.md"));
30    if !path.is_file() {
31        bail!("ticket '{slug}' not found at {}", path.display());
32    }
33    let note = ticket_notes::append_note(repo, slug, &note_author(), text)?;
34    Ok(format!("noted on '{slug}' at {}\n", note.ts.to_rfc3339()))
35}
36
37/// `kranz ticket notes <slug>`: print the discussion chronologically.
38pub fn cmd_ticket_notes(repo: &Path, slug: &str) -> Result<String> {
39    let notes = ticket_notes::read_notes(repo, slug)?;
40    Ok(render_ticket_notes(slug, &notes))
41}
42
43/// Render the notes listing: one `<ts>  <author>: <text>` line per note, in
44/// file order (== chronological order).
45pub fn render_ticket_notes(slug: &str, notes: &[TicketNote]) -> String {
46    if notes.is_empty() {
47        return format!("no notes on '{slug}'\n");
48    }
49    let mut out = format!("notes on '{slug}' ({}):\n", notes.len());
50    for note in notes {
51        out.push_str(&format!(
52            "{}  {}: {}\n",
53            note.ts.to_rfc3339(),
54            note.author,
55            note.text
56        ));
57    }
58    out
59}