cuj-tui 0.1.0

cui — a read-only TUI browser for cuj vaults
Documentation
//! The right panel: metadata strip on top, content below.

use metatheca::{Uuid, format_iso8601_secs};
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};

use crate::snapshot::JotRow;
use crate::state::{AppState, Focus};
use crate::ui::border_style;

pub fn draw(f: &mut Frame, area: Rect, state: &mut AppState) {
    let block = Block::bordered().border_style(border_style(state.focus == Focus::View));
    let inner = block.inner(area);
    f.render_widget(block, area);
    if inner.height < 3 {
        state.view_height = 0;
        return;
    }
    let [meta, content] =
        Layout::vertical([Constraint::Length(2), Constraint::Min(0)]).areas(inner);
    let cblock = Block::new()
        .borders(Borders::TOP)
        .border_style(Style::new().add_modifier(Modifier::DIM));
    let cinner = cblock.inner(content);
    state.view_height = cinner.height;
    f.render_widget(cblock, content);

    let Some(row) = state.selected_row() else {
        f.render_widget(
            Paragraph::new("no jot selected").style(Style::new().add_modifier(Modifier::DIM)),
            meta,
        );
        return;
    };

    f.render_widget(
        Paragraph::new(vec![meta_line_1(row), meta_line_2(row, state)]),
        meta,
    );

    let lines = state
        .selected_row()
        .and_then(|r| state.content_of(r.entry))
        .map(|c| c.lines.clone())
        .unwrap_or_default();
    f.render_widget(
        Paragraph::new(lines)
            .wrap(Wrap { trim: false })
            .scroll((state.view_scroll, 0)),
        cinner,
    );
}

/// `--work--12 (md)   created 2026-07-01 09:14:02   2 att   1/3 todos`
fn meta_line_1(row: &JotRow) -> Line<'static> {
    let mut spans = vec![
        Span::styled(
            format!("--{}--{}", row.profile, row.id),
            Style::new().add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!(" ({})", row.ext),
            Style::new().add_modifier(Modifier::DIM),
        ),
    ];
    if let Some(ns) = row.created_at_ns {
        spans.push(Span::raw("   created "));
        spans.push(Span::styled(
            format_iso8601_secs(ns),
            Style::new().add_modifier(Modifier::DIM),
        ));
    }
    if !row.attachments.is_empty() {
        spans.push(Span::raw(format!("   {} att", row.attachments.len())));
    }
    let todos = &row.effective.todos;
    if !todos.is_empty() {
        let done = todos.iter().filter(|t| t.done).count();
        spans.push(Span::raw(format!("   {done}/{} todos", todos.len())));
    }
    Line::from(spans)
}

/// `..tag ..tag ::work::alpha   refs: --3 --misc--7`
fn meta_line_2(row: &JotRow, state: &AppState) -> Line<'static> {
    let e = &row.effective;
    let mut spans = Vec::new();
    for t in &e.tags {
        spans.push(Span::styled(
            format!("..{t}"),
            Style::new().fg(Color::Yellow),
        ));
        spans.push(Span::raw(" "));
    }
    for c in &e.categories {
        spans.push(Span::styled(
            format!("::{c}"),
            Style::new().fg(Color::Green),
        ));
        spans.push(Span::raw(" "));
    }
    if !e.edges.is_empty() {
        spans.push(Span::styled(
            "  refs: ",
            Style::new().add_modifier(Modifier::DIM),
        ));
        // One span per effective edge (SPEC §5.5): typed edges to
        // one target render once each, category-suffixed.
        for (entry_str, infos) in &e.edges {
            for info in infos {
                let mut label = edge_ref(state, entry_str, &info.written_as);
                if let Some(cat) = &info.annot.category {
                    label.push_str("::");
                    label.push_str(cat);
                }
                spans.push(Span::styled(label, Style::new().fg(Color::Cyan)));
                spans.push(Span::raw(" "));
            }
        }
    }
    if !row.attachments.is_empty() {
        spans.push(Span::styled(
            "  att: ",
            Style::new().add_modifier(Modifier::DIM),
        ));
        for a in &row.attachments {
            spans.push(Span::styled(a.clone(), Style::new().fg(Color::Magenta)));
            spans.push(Span::raw(" "));
        }
    }
    if spans.is_empty() {
        spans.push(Span::styled(
            "(no metadata)",
            Style::new().add_modifier(Modifier::DIM),
        ));
    }
    Line::from(spans)
}

/// Render an edge by its target's current identity; fall back to
/// the written form (marked) when the target is not a live jot.
fn edge_ref(state: &AppState, entry_str: &str, written: &str) -> String {
    Uuid::parse_str(entry_str)
        .ok()
        .and_then(|u| state.snapshot.by_entry.get(&u))
        .map(|&i| state.snapshot.rows[i].short_ref(state.scope()))
        .unwrap_or_else(|| format!("{written}?"))
}