mermaid_cli/render/widgets/
conversation_list.rs1use ratatui::buffer::Buffer;
9use ratatui::layout::Rect;
10use ratatui::style::{Color, Modifier, Style};
11use ratatui::text::{Line, Span};
12use ratatui::widgets::{Block, Borders, Paragraph, Widget};
13
14use crate::domain::ConversationSummary;
15use crate::render::theme::Theme;
16
17pub struct ConversationListWidget<'a> {
18 pub theme: &'a Theme,
19 pub candidates: &'a [ConversationSummary],
20 pub cursor: usize,
21}
22
23impl<'a> Widget for ConversationListWidget<'a> {
24 fn render(self, area: Rect, buf: &mut Buffer) {
25 let title = if self.candidates.is_empty() {
26 "Load conversation — (none found)"
27 } else {
28 "Load conversation — ↑↓ navigate · Enter select · Esc cancel"
29 };
30 let block = Block::default()
31 .borders(Borders::ALL)
32 .title(title)
33 .border_style(Style::default().fg(self.theme.colors.border.to_color()));
34
35 let inner_height = area.height.saturating_sub(2) as usize;
37 let visible = inner_height.min(10);
38 let start = if self.cursor >= visible {
39 self.cursor + 1 - visible
40 } else {
41 0
42 };
43
44 let rows: Vec<Line<'_>> = self
45 .candidates
46 .iter()
47 .enumerate()
48 .skip(start)
49 .take(visible)
50 .map(|(i, summary)| {
51 let highlighted = i == self.cursor;
52 let prefix = if highlighted { " > " } else { " " };
53 let row_style = if highlighted {
54 Style::default()
55 .bg(self.theme.colors.text_disabled.to_color())
56 .add_modifier(Modifier::BOLD)
57 } else {
58 Style::default()
59 };
60 let title = truncate(&summary.title, 48);
61 let meta = format!(
62 " ({} msg · {})",
63 summary.message_count,
64 short_timestamp(&summary.updated_at)
65 );
66 Line::from(vec![
67 Span::raw(prefix),
68 Span::styled(title, row_style.fg(Color::White)),
69 Span::styled(
70 meta,
71 row_style.fg(self.theme.colors.text_disabled.to_color()),
72 ),
73 ])
74 })
75 .collect();
76
77 Paragraph::new(rows).block(block).render(area, buf);
78 }
79}
80
81fn truncate(s: &str, max: usize) -> String {
82 if s.chars().count() <= max {
83 s.to_string()
84 } else {
85 let cut = s.floor_char_boundary(max);
86 format!("{}…", &s[..cut])
87 }
88}
89
90fn short_timestamp(rfc3339: &str) -> String {
93 if rfc3339.len() >= 16 {
96 let mut s = rfc3339[..16].to_string();
97 if let Some(t_pos) = s.find('T') {
98 s.replace_range(t_pos..t_pos + 1, " ");
99 }
100 s
101 } else {
102 rfc3339.to_string()
103 }
104}