use anyhow::Result;
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Frame, Terminal,
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph},
};
use std::io;
use super::conversation::ConversationHistory;
pub fn select_conversation(
conversations: Vec<ConversationHistory>,
) -> Result<Option<ConversationHistory>> {
if conversations.is_empty() {
println!("No previous conversations found in this directory.");
return Ok(None);
}
if conversations.len() == 1 {
return Ok(conversations.into_iter().next());
}
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = ConversationSelector {
conversations,
selected: 0,
};
let result = run_selector(&mut terminal, &mut app);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
result
}
struct ConversationSelector {
conversations: Vec<ConversationHistory>,
selected: usize,
}
fn run_selector(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut ConversationSelector,
) -> Result<Option<ConversationHistory>> {
loop {
terminal.draw(|f| render_selector(f, app))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
return Ok(None);
},
KeyCode::Enter => {
let selected = app.conversations[app.selected].clone();
return Ok(Some(selected));
},
KeyCode::Down | KeyCode::Char('j') => {
if app.selected < app.conversations.len() - 1 {
app.selected += 1;
}
},
KeyCode::Up | KeyCode::Char('k') => {
if app.selected > 0 {
app.selected -= 1;
}
},
KeyCode::Home => {
app.selected = 0;
},
KeyCode::End => {
app.selected = app.conversations.len() - 1;
},
_ => {},
}
}
}
}
fn render_selector(f: &mut Frame, app: &ConversationSelector) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(5),
Constraint::Length(3),
]);
let [title_area, list_area, help_area] = f.area().layout(&layout);
let title = Paragraph::new("Select a conversation to resume")
.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Mermaid - Resume Session "),
);
f.render_widget(title, title_area);
let items: Vec<ListItem> = app
.conversations
.iter()
.enumerate()
.map(|(i, conv)| {
let style = if i == app.selected {
Style::default()
.bg(Color::Blue)
.fg(Color::White)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let content = vec![
Line::from(vec![Span::styled(&conv.title, style)]),
Line::from(vec![Span::styled(
format!(
" {} | {} messages | Model: {}",
conv.updated_at.format("%Y-%m-%d %H:%M"),
conv.messages.len(),
conv.model_name
),
style.fg(Color::Gray),
)]),
];
ListItem::new(content)
})
.collect();
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Previous Conversations "),
)
.highlight_style(Style::default())
.highlight_symbol("");
f.render_widget(list, list_area);
let help = vec![Line::from(vec![
Span::raw("Up/k: Up Down/j: Down "),
Span::styled("Enter", Style::default().fg(Color::Green)),
Span::raw(": Select "),
Span::styled("q/Esc", Style::default().fg(Color::Red)),
Span::raw(": Cancel"),
])];
let help_widget = Paragraph::new(help)
.style(Style::default().fg(Color::DarkGray))
.block(Block::default().borders(Borders::ALL));
f.render_widget(help_widget, help_area);
}