use anki_bridge::{AnkiClient, AnkiRequestable, prelude::*}; use crossterm::{ cursor, event::{self, Event, KeyCode}, execute, style::*, terminal::*, }; use nanohtml2text::html2text; use std::{ io::stdout, process::{Command, Stdio}, thread, time::Duration, }; const GOOD: char = '3'; const AGAIN: char = '1'; fn main() { let anki = AnkiClient::default(); init(&anki); loop { prompt(&anki); } } fn init(anki: &AnkiClient) { clear_screen(); let mut decks = anki.request(DeckNamesRequest {}); if let Err(_) = decks { clear_screen(); display_text("initializing headless anki..."); Command::new("bash") .arg("-c") .arg("QT_QPA_PLATFORM=xcb xvfb-run -a anki") .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .unwrap(); loop { thread::sleep(Duration::from_secs(1)); display_text("."); decks = anki.request(DeckNamesRequest {}); if let Ok(_) = decks { clear_screen(); break; } } } let decks = decks.unwrap(); for (index, title) in decks.clone().into_iter().enumerate() { // 0th index is Default, which is not predictable if index > 0 { println!("{:?}: {}", index, title); } } display_prompt_text("deck index:"); let mut input = "".to_string(); loop { match event::read().unwrap() { Event::Key(e) => match e.code { KeyCode::Char(c) => input = input + &c.to_string(), KeyCode::Enter => break, _ => (), }, _ => (), } } clear_screen(); display_text("loading..."); anki.request(GuiDeckReviewRequest { name: decks[input.parse::().unwrap()].clone(), }) .unwrap(); } fn prompt(anki: &AnkiClient) { flush_read(); // needs to be done twice to account for lag on the other end anki.request(GuiCurrentCardRequest {}).unwrap(); let card = anki.request(GuiCurrentCardRequest {}).unwrap(); clear_with_bar(&anki, &card); display_html(&card.question); loop { match event::read().unwrap() { Event::Key(e) => match e.code { KeyCode::Enter => break, KeyCode::Char('u') => { anki.request(GuiUndoRequest {}).unwrap(); return; } _ => (), }, _ => (), }; } anki.request(GuiShowAnswerRequest {}).unwrap(); clear_with_bar(&anki, &card); { let length = html2text(&card.question).len(); let text = &html2text(&card.answer)[(2 + length)..]; display_text(&text); } display_prompt_text(":"); let ease = loop { match event::read().unwrap() { Event::Key(e) => match e.code { KeyCode::Char(AGAIN) => break Ease::Again, KeyCode::Char(GOOD) => break Ease::Good, KeyCode::Char('u') => { anki.request(GuiUndoRequest {}).unwrap(); return; } _ => (), }, _ => (), }; }; anki.request(GuiAnswerCardRequest { ease: ease }).unwrap(); } fn flush_read() { loop { if event::poll(Duration::from_secs(0)).unwrap() { event::read().unwrap(); } else { break; } } } fn clear_with_bar(anki: &AnkiClient, current_card: &GuiCurrentCardResponse) { let deck_name = ¤t_card.deck_name; let deck_stats = anki .request(GetDeckStatsRequest { decks: vec![deck_name.to_string()], }) .unwrap() .into_iter() .next() .unwrap() .1; clear_screen(); execute!( stdout(), SetForegroundColor(Color::Blue), Print(&format!("{:?} ", deck_stats.new_count)), SetForegroundColor(Color::Red), Print(&format!("{:?} ", deck_stats.learn_count)), SetForegroundColor(Color::Green), Print(&format!("{:?}\n", deck_stats.review_count)), ResetColor ) .unwrap(); } fn clear_screen() { execute!( stdout(), Clear(ClearType::All), cursor::MoveTo(0, 0), cursor::Hide ) .unwrap(); } fn display_prompt_text(text: &str) { execute!( stdout(), SetForegroundColor(Color::Blue), Print(text), ResetColor ) .unwrap(); } fn display_html(html: &str) { let text = html2text(html); display_text(&text); } fn display_text(text: &str) { execute!( stdout(), SetForegroundColor(Color::DarkYellow), Print(text), ResetColor ) .unwrap(); }