cotext 0.1.3

Structured project context for humans and coding agents, with a CLI and ratatui TUI.
Documentation
use std::collections::BTreeMap;
use std::fmt::Write;

use chrono::Local;

use crate::model::{Audience, Category, Entry, EntryStatus};
use crate::storage::Project;

pub fn render_packet(project: &Project, entries: &[Entry], audience: Audience) -> String {
    let mut grouped: BTreeMap<Category, Vec<&Entry>> = BTreeMap::new();
    for category in Category::ALL {
        grouped.entry(category).or_default();
    }
    for entry in entries {
        grouped.entry(entry.category()).or_default().push(entry);
    }

    let mut out = String::new();
    let generated_at = Local::now().format("%Y-%m-%d %H:%M:%S %Z");
    let title = match audience {
        Audience::Human => "Cotext Project View",
        Audience::Agent => "Cotext Agent Packet",
        Audience::Codex => "Cotext Packet for Codex",
        Audience::Claude => "Cotext Packet for Claude Code",
    };
    let _ = writeln!(&mut out, "# {title}");
    let _ = writeln!(&mut out);
    let _ = writeln!(&mut out, "- Project: {}", project.config.name);
    let _ = writeln!(&mut out, "- Root: `{}`", project.root.display());
    let _ = writeln!(
        &mut out,
        "- Storage: {} (`{}`)",
        project.storage_scope(),
        project.data_dir_display()
    );
    let _ = writeln!(&mut out, "- Generated: {generated_at}");
    let _ = writeln!(
        &mut out,
        "- Update flow: `cotext new`, `cotext update`, or `cotext tui`"
    );
    let _ = writeln!(&mut out);
    match audience {
        Audience::Human => {
            let _ = writeln!(
                &mut out,
                "Use this as the single source of truth for design, awareness notes, progress, next work, and deferred work."
            );
        }
        Audience::Agent | Audience::Codex | Audience::Claude => {
            let _ = writeln!(
                &mut out,
                "Read this packet before coding. When work changes design, notes, progress, or todo state, write the update back through cotext instead of scattering raw markdown."
            );
        }
    }
    let _ = writeln!(&mut out);
    let _ = writeln!(&mut out, "## Snapshot");
    let _ = writeln!(&mut out);
    for category in Category::ALL {
        let items = grouped.get(&category).map(Vec::as_slice).unwrap_or(&[]);
        let open = items
            .iter()
            .filter(|entry| !matches!(entry.status(), EntryStatus::Done | EntryStatus::Archived))
            .count();
        let _ = writeln!(
            &mut out,
            "- {}: {} entries, {} open",
            category.label(),
            items.len(),
            open
        );
    }

    for category in Category::ALL {
        let _ = writeln!(&mut out);
        let _ = writeln!(&mut out, "## {}", category.label());
        let _ = writeln!(&mut out);
        let items = grouped.get(&category).map(Vec::as_slice).unwrap_or(&[]);
        if items.is_empty() {
            let _ = writeln!(&mut out, "_None yet._");
            continue;
        }
        for entry in items {
            render_entry(&mut out, entry);
        }
    }

    out
}

pub fn render_single_entry(entry: &Entry) -> String {
    let mut out = String::new();
    render_entry(&mut out, entry);
    out
}

pub fn render_clipboard_packet(project: &Project, entries: &[Entry], audience: Audience) -> String {
    let mut rendered = String::new();
    let _ = writeln!(&mut rendered, "BEGIN_COTEXT_PACKET");
    rendered.push_str(&render_packet(project, entries, audience));
    let _ = writeln!(&mut rendered, "END_COTEXT_PACKET");
    rendered
}

fn render_entry(out: &mut String, entry: &Entry) {
    let _ = writeln!(out, "### {} (`{}`)", entry.title(), entry.id());
    let _ = writeln!(out);
    let _ = writeln!(out, "- Status: {}", entry.status());
    if let Some(section) = entry.section() {
        let _ = writeln!(out, "- Section: `{section}`");
    }
    if !entry.front_matter.tags.is_empty() {
        let tags = entry
            .front_matter
            .tags
            .iter()
            .map(String::as_str)
            .collect::<Vec<_>>()
            .join(", ");
        let _ = writeln!(out, "- Tags: `{tags}`");
    }
    let _ = writeln!(
        out,
        "- Updated: {}",
        entry.front_matter.updated_at.format("%Y-%m-%d %H:%M UTC")
    );
    let _ = writeln!(out);
    let _ = writeln!(out, "{}", entry.body.trim());
    let _ = writeln!(out);
}