mermaid_cli/session/
selector.rs1use anyhow::Result;
2use crossterm::{
3 event::{self, Event, KeyCode},
4 execute,
5 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
6};
7use ratatui::{
8 backend::CrosstermBackend,
9 layout::{Constraint, Direction, Layout},
10 style::{Color, Modifier, Style},
11 text::{Line, Span},
12 widgets::{Block, Borders, List, ListItem, Paragraph},
13 Frame, Terminal,
14};
15use std::io;
16
17use super::conversation::ConversationHistory;
18
19pub fn select_conversation(
21 conversations: Vec<ConversationHistory>,
22) -> Result<Option<ConversationHistory>> {
23 if conversations.is_empty() {
24 println!("No previous conversations found in this directory.");
25 return Ok(None);
26 }
27
28 if conversations.len() == 1 {
30 return Ok(conversations.into_iter().next());
31 }
32
33 enable_raw_mode()?;
35 let mut stdout = io::stdout();
36 execute!(stdout, EnterAlternateScreen)?;
37 let backend = CrosstermBackend::new(stdout);
38 let mut terminal = Terminal::new(backend)?;
39
40 let mut app = ConversationSelector {
42 conversations,
43 selected: 0,
44 };
45
46 let result = run_selector(&mut terminal, &mut app);
48
49 disable_raw_mode()?;
51 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
52 terminal.show_cursor()?;
53
54 result
55}
56
57struct ConversationSelector {
58 conversations: Vec<ConversationHistory>,
59 selected: usize,
60}
61
62fn run_selector(
63 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
64 app: &mut ConversationSelector,
65) -> Result<Option<ConversationHistory>> {
66 loop {
67 terminal.draw(|f| render_selector(f, app))?;
68
69 if let Event::Key(key) = event::read()? {
70 match key.code {
71 KeyCode::Char('q') | KeyCode::Esc => {
72 return Ok(None);
73 },
74 KeyCode::Enter => {
75 let selected = app.conversations[app.selected].clone();
76 return Ok(Some(selected));
77 },
78 KeyCode::Down | KeyCode::Char('j') => {
79 if app.selected < app.conversations.len() - 1 {
80 app.selected += 1;
81 }
82 },
83 KeyCode::Up | KeyCode::Char('k') => {
84 if app.selected > 0 {
85 app.selected -= 1;
86 }
87 },
88 KeyCode::Home => {
89 app.selected = 0;
90 },
91 KeyCode::End => {
92 app.selected = app.conversations.len() - 1;
93 },
94 _ => {},
95 }
96 }
97 }
98}
99
100fn render_selector(f: &mut Frame, app: &ConversationSelector) {
101 let chunks = Layout::default()
102 .direction(Direction::Vertical)
103 .constraints([
104 Constraint::Length(3),
105 Constraint::Min(5),
106 Constraint::Length(3),
107 ])
108 .split(f.area());
109
110 let title = Paragraph::new("Select a conversation to resume")
112 .style(
113 Style::default()
114 .fg(Color::Cyan)
115 .add_modifier(Modifier::BOLD),
116 )
117 .block(
118 Block::default()
119 .borders(Borders::ALL)
120 .title(" Mermaid - Resume Session "),
121 );
122 f.render_widget(title, chunks[0]);
123
124 let items: Vec<ListItem> = app
126 .conversations
127 .iter()
128 .enumerate()
129 .map(|(i, conv)| {
130 let style = if i == app.selected {
131 Style::default()
132 .bg(Color::Blue)
133 .fg(Color::White)
134 .add_modifier(Modifier::BOLD)
135 } else {
136 Style::default()
137 };
138
139 let content = vec![
140 Line::from(vec![Span::styled(&conv.title, style)]),
141 Line::from(vec![Span::styled(
142 format!(
143 " {} | {} messages | Model: {}",
144 conv.updated_at.format("%Y-%m-%d %H:%M"),
145 conv.messages.len(),
146 conv.model_name
147 ),
148 style.fg(Color::Gray),
149 )]),
150 ];
151
152 ListItem::new(content)
153 })
154 .collect();
155
156 let list = List::new(items)
157 .block(
158 Block::default()
159 .borders(Borders::ALL)
160 .title(" Previous Conversations "),
161 )
162 .highlight_style(Style::default())
163 .highlight_symbol("");
164
165 f.render_widget(list, chunks[1]);
166
167 let help = vec![Line::from(vec![
169 Span::raw("Up/k: Up Down/j: Down "),
170 Span::styled("Enter", Style::default().fg(Color::Green)),
171 Span::raw(": Select "),
172 Span::styled("q/Esc", Style::default().fg(Color::Red)),
173 Span::raw(": Cancel"),
174 ])];
175 let help_widget = Paragraph::new(help)
176 .style(Style::default().fg(Color::DarkGray))
177 .block(Block::default().borders(Borders::ALL));
178 f.render_widget(help_widget, chunks[2]);
179}