use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Frame, Terminal,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, List, ListItem, ListState},
};
use std::io;
use crate::models::ProblemSummary;
pub struct App {
pub should_quit: bool,
pub problems: Vec<ProblemSummary>,
pub list_state: ListState,
pub selected_problem: Option<String>,
}
impl App {
pub fn new(problems: Vec<ProblemSummary>) -> Self {
let mut list_state = ListState::default();
if !problems.is_empty() {
list_state.select(Some(0)); }
Self {
should_quit: false,
selected_problem: None,
problems,
list_state,
}
}
pub fn next(&mut self) {
let i = match self.list_state.selected() {
Some(i) => {
if i >= self.problems.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.list_state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.list_state.selected() {
Some(i) => {
if i == 0 {
self.problems.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.list_state.select(Some(i));
}
}
pub async fn run_tui(
problems: Vec<ProblemSummary>,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
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 = App::new(problems);
let res = run_app(&mut terminal, &mut app).await;
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
if let Err(err) = res {
eprintln!("Error running TUI: {:?}", err);
}
Ok(app.selected_problem)
}
async fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
loop {
let _ = terminal.draw(|f| ui(f, app));
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
KeyCode::Down | KeyCode::Char('j') => app.next(),
KeyCode::Up | KeyCode::Char('k') => app.previous(),
KeyCode::Enter => {
if let Some(i) = app.list_state.selected() {
app.selected_problem = Some(app.problems[i].slug.clone());
app.should_quit = true;
}
}
_ => {}
}
}
}
if app.should_quit {
return Ok(());
}
}
}
fn ui(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Length(2), Constraint::Min(1)])
.split(f.area());
let items: Vec<ListItem> = app
.problems
.iter()
.map(|p| {
let diff_color = match p.difficulty {
1 => Color::Green,
2 => Color::Yellow,
_ => Color::Red,
};
let line = format!("[{}] {}", p.id, p.title);
ListItem::new(line).style(Style::default().fg(diff_color))
})
.collect();
let list = List::new(items)
.block(Block::default().title(" Problems ").borders(Borders::ALL))
.highlight_style(Style::default().bg(Color::DarkGray).fg(Color::White))
.highlight_symbol(">> ");
f.render_stateful_widget(list, chunks[1], &mut app.list_state);
}