use std::io::{self, Stdout};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use learner::{
database::{OrderField, Query},
format::format_title,
Learner,
};
use ratatui::{backend::CrosstermBackend, widgets::ListState, Terminal};
use super::*;
mod state;
mod styles;
mod ui;
use interaction::{ResponseContent, UserInteraction};
use state::{DialogType, UIState};
use ui::UIDrawer;
pub struct Tui {
terminal: Terminal<CrosstermBackend<Stdout>>,
state: UIState,
learner: Learner,
}
impl Tui {
pub async fn new(mut learner: Learner) -> Result<Self> {
let papers =
Query::list_all().order_by(OrderField::Title).execute(&mut learner.database).await?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let terminal = Terminal::new(CrosstermBackend::new(stdout))?;
Ok(Self { terminal, state: UIState::new(papers), learner })
}
pub async fn run(&mut self) -> Result<()> {
loop {
if let Some(cmd) = self.state.pending_command.take() {
if let Err(e) = self.execute_command(cmd).await {
self.state.set_status_message(format!("Error: {}", e));
}
}
if self.state.needs_redraw {
self.terminal.draw(|f| UIDrawer::new(f, &mut self.state).draw())?;
}
if event::poll(std::time::Duration::from_millis(1))? {
match event::read()? {
Event::Key(key) =>
if self.state.handle_input(key.code, key.modifiers) {
break;
},
Event::Resize(..) => self.state.needs_redraw = true,
_ => {},
}
}
}
self.cleanup()?;
Ok(())
}
fn cleanup(&mut self) -> Result<()> {
disable_raw_mode()?;
execute!(self.terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
self.terminal.show_cursor()?;
Ok(())
}
pub async fn execute_command(&mut self, command: Commands) -> Result<()> {
match command {
Commands::Add(args) => {
if !args.pdf && !args.no_pdf {
let paper = add(self, AddArgs { no_pdf: true, ..args }).await?;
self.state.dialog = DialogType::PDFConfirm { paper };
} else {
add(self, args).await?;
self.state.dialog =
DialogType::Success { message: "Paper added successfully".to_string() };
}
self.refresh_papers().await?;
},
Commands::Remove(args) => {
if !args.force {
let mut matching_papers = Vec::new();
let papers = Query::text(&args.query).execute(&mut self.learner.database).await?;
for paper in papers {
if let Some(author) = &args.filter.author {
if !paper.authors.iter().any(|a| a.name.contains(author)) {
continue;
}
}
if let Some(source) = &args.filter.source {
if &paper.source.to_string() != source {
continue;
}
}
if let Some(before) = &args.filter.before {
if !paper.publication_date.to_string().starts_with(before) {
continue;
}
}
matching_papers.push(paper);
}
if matching_papers.is_empty() {
self.state.set_status_message("No papers found matching criteria".to_string());
} else {
self.state.dialog = DialogType::RemoveConfirm { papers: matching_papers, args };
}
} else {
remove(self, args).await?;
self.state.dialog =
DialogType::Success { message: "Papers removed successfully".to_string() };
self.refresh_papers().await?;
}
},
Commands::Search(args) => {
let mut papers = Query::text(&args.query).execute(&mut self.learner.database).await?;
if let Some(author) = &args.filter.author {
papers.retain(|p| p.authors.iter().any(|a| a.name.contains(author)));
}
if let Some(source) = &args.filter.source {
papers.retain(|p| &p.source.to_string() == source);
}
if let Some(before) = &args.filter.before {
papers.retain(|p| p.publication_date.to_string().starts_with(before));
}
if papers.is_empty() {
self.state.set_status_message("No papers found matching criteria".to_string());
} else {
let mut selected = ListState::default();
selected.select(Some(0));
self.state.dialog = DialogType::SearchResults { query: args.query, papers, selected };
}
},
_ => return Err(LearnerdError::Daemon("Command not supported in TUI mode".to_string())),
}
Ok(())
}
async fn refresh_papers(&mut self) -> Result<()> {
self.state.papers =
Query::list_all().order_by(OrderField::Title).execute(&mut self.learner.database).await?;
self.state.needs_redraw = true;
Ok(())
}
}
impl UserInteraction for Tui {
fn learner(&mut self) -> &mut Learner { &mut self.learner }
fn confirm(&mut self, message: &str) -> Result<bool> {
self.state.set_status_message(format!("Confirm: {}", message));
Ok(true)
}
fn prompt(&mut self, message: &str) -> Result<String> {
self.state.set_status_message(format!("Prompt: {}", message));
Ok(String::new())
}
fn reply(&mut self, content: ResponseContent) -> Result<()> {
match content {
ResponseContent::Success(msg) => {
self.state.set_status_message(msg.to_string());
},
ResponseContent::Error(e) => {
self.state.set_status_message(format!("Error: {}", e));
},
ResponseContent::Info(msg) => {
self.state.set_status_message(msg.to_string());
},
ResponseContent::Paper(paper) => {
self.state.set_status_message(format!("Paper: {}", paper.title));
},
ResponseContent::Papers(papers) => {
self.state.set_status_message(format!("Found {} papers", papers.len()));
},
}
self.state.needs_redraw = true;
Ok(())
}
}
pub async fn run(learner: Learner) -> Result<()> {
let mut tui = Tui::new(learner).await?;
tui.run().await
}