1use metatheca::{Uuid, format_iso8601_secs};
4use ratatui::Frame;
5use ratatui::layout::{Constraint, Layout, Rect};
6use ratatui::style::{Color, Modifier, Style};
7use ratatui::text::{Line, Span};
8use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
9
10use crate::snapshot::JotRow;
11use crate::state::{AppState, Focus};
12use crate::ui::border_style;
13
14pub fn draw(f: &mut Frame, area: Rect, state: &mut AppState) {
15 let block = Block::bordered().border_style(border_style(state.focus == Focus::View));
16 let inner = block.inner(area);
17 f.render_widget(block, area);
18 if inner.height < 3 {
19 state.view_height = 0;
20 return;
21 }
22 let [meta, content] =
23 Layout::vertical([Constraint::Length(2), Constraint::Min(0)]).areas(inner);
24 let cblock = Block::new()
25 .borders(Borders::TOP)
26 .border_style(Style::new().add_modifier(Modifier::DIM));
27 let cinner = cblock.inner(content);
28 state.view_height = cinner.height;
29 f.render_widget(cblock, content);
30
31 let Some(row) = state.selected_row() else {
32 f.render_widget(
33 Paragraph::new("no jot selected").style(Style::new().add_modifier(Modifier::DIM)),
34 meta,
35 );
36 return;
37 };
38
39 f.render_widget(
40 Paragraph::new(vec![meta_line_1(row), meta_line_2(row, state)]),
41 meta,
42 );
43
44 let lines = state
45 .selected_row()
46 .and_then(|r| state.content_of(r.entry))
47 .map(|c| c.lines.clone())
48 .unwrap_or_default();
49 f.render_widget(
50 Paragraph::new(lines)
51 .wrap(Wrap { trim: false })
52 .scroll((state.view_scroll, 0)),
53 cinner,
54 );
55}
56
57fn meta_line_1(row: &JotRow) -> Line<'static> {
59 let mut spans = vec![
60 Span::styled(
61 format!("--{}--{}", row.profile, row.id),
62 Style::new().add_modifier(Modifier::BOLD),
63 ),
64 Span::styled(
65 format!(" ({})", row.ext),
66 Style::new().add_modifier(Modifier::DIM),
67 ),
68 ];
69 if let Some(ns) = row.created_at_ns {
70 spans.push(Span::raw(" created "));
71 spans.push(Span::styled(
72 format_iso8601_secs(ns),
73 Style::new().add_modifier(Modifier::DIM),
74 ));
75 }
76 if !row.attachments.is_empty() {
77 spans.push(Span::raw(format!(" {} att", row.attachments.len())));
78 }
79 let todos = &row.effective.todos;
80 if !todos.is_empty() {
81 let done = todos.iter().filter(|t| t.done).count();
82 spans.push(Span::raw(format!(" {done}/{} todos", todos.len())));
83 }
84 Line::from(spans)
85}
86
87fn meta_line_2(row: &JotRow, state: &AppState) -> Line<'static> {
89 let e = &row.effective;
90 let mut spans = Vec::new();
91 for t in &e.tags {
92 spans.push(Span::styled(
93 format!("..{t}"),
94 Style::new().fg(Color::Yellow),
95 ));
96 spans.push(Span::raw(" "));
97 }
98 for c in &e.categories {
99 spans.push(Span::styled(
100 format!("::{c}"),
101 Style::new().fg(Color::Green),
102 ));
103 spans.push(Span::raw(" "));
104 }
105 if !e.edges.is_empty() {
106 spans.push(Span::styled(
107 " refs: ",
108 Style::new().add_modifier(Modifier::DIM),
109 ));
110 for (entry_str, infos) in &e.edges {
113 for info in infos {
114 let mut label = edge_ref(state, entry_str, &info.written_as);
115 if let Some(cat) = &info.annot.category {
116 label.push_str("::");
117 label.push_str(cat);
118 }
119 spans.push(Span::styled(label, Style::new().fg(Color::Cyan)));
120 spans.push(Span::raw(" "));
121 }
122 }
123 }
124 if !row.attachments.is_empty() {
125 spans.push(Span::styled(
126 " att: ",
127 Style::new().add_modifier(Modifier::DIM),
128 ));
129 for a in &row.attachments {
130 spans.push(Span::styled(a.clone(), Style::new().fg(Color::Magenta)));
131 spans.push(Span::raw(" "));
132 }
133 }
134 if spans.is_empty() {
135 spans.push(Span::styled(
136 "(no metadata)",
137 Style::new().add_modifier(Modifier::DIM),
138 ));
139 }
140 Line::from(spans)
141}
142
143fn edge_ref(state: &AppState, entry_str: &str, written: &str) -> String {
146 Uuid::parse_str(entry_str)
147 .ok()
148 .and_then(|u| state.snapshot.by_entry.get(&u))
149 .map(|&i| state.snapshot.rows[i].short_ref(state.scope()))
150 .unwrap_or_else(|| format!("{written}?"))
151}