diff options
author | Joris | 2021-11-14 23:25:55 +0100 |
---|---|---|
committer | Joris | 2021-11-19 11:42:20 +0100 |
commit | 9f94611a42d41cf94cdccb00b5d2eec0d5d02970 (patch) | |
tree | 9bab5bc342e22aa38b13a2dbd3525bbfe2beedb5 /src/gui | |
parent | 59c44b15010eea5490896a5b5d427b415ad6f56a (diff) |
Add initial working version
Diffstat (limited to 'src/gui')
-rw-r--r-- | src/gui/gui.rs | 34 | ||||
-rw-r--r-- | src/gui/mod.rs | 2 | ||||
-rw-r--r-- | src/gui/question.rs | 198 |
3 files changed, 234 insertions, 0 deletions
diff --git a/src/gui/gui.rs b/src/gui/gui.rs new file mode 100644 index 0000000..fdf686b --- /dev/null +++ b/src/gui/gui.rs @@ -0,0 +1,34 @@ +use crate::{db::db, gui::question, space_repetition, util::event::Events}; +use anyhow::Result; +use rusqlite::Connection; +use std::io; +use termion::{raw::IntoRawMode, screen::AlternateScreen}; +use tui::{backend::TermionBackend, Terminal}; + +pub fn start(conn: &Connection) -> Result<()> { + let stdout = io::stdout().into_raw_mode()?; + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let events = Events::new(); + + loop { + match db::pick_ready(&conn) { + Some(card) => { + let difficulty = + question::ask(&mut terminal, &events, &card, "German".to_string())?; + db::update( + &conn, + &card.question, + &space_repetition::update(card.state, difficulty), + )?; + } + None => { + break; + } + } + } + + Ok(()) +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..cbf9675 --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,2 @@ +pub mod gui; +pub mod question; diff --git a/src/gui/question.rs b/src/gui/question.rs new file mode 100644 index 0000000..a22b977 --- /dev/null +++ b/src/gui/question.rs @@ -0,0 +1,198 @@ +use crate::{ + model::{card::Card, difficulty, difficulty::Difficulty}, + util::event::{Event, Events}, + util::serialization, +}; +use anyhow::Result; +use termion::event::Key; +use tui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, Borders, Paragraph, Wrap}, + Terminal, +}; + +struct State { + pub input: String, + pub answer: Answer, +} + +enum Answer { + Write, + Difficulty { difficulty: Difficulty }, +} + +pub fn ask<B: Backend>( + terminal: &mut Terminal<B>, + events: &Events, + card: &Card, + deck: String, +) -> Result<Difficulty> { + let mut state = State { + input: String::new(), + answer: Answer::Write, + }; + + loop { + terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(1), + Constraint::Percentage(30), + Constraint::Length(5), + Constraint::Percentage(30), + Constraint::Length(5), + ] + .as_ref(), + ) + .split(f.size()); + + let d1 = Paragraph::new(format!("{}", deck)) + .alignment(Alignment::Center) + .style( + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ); + f.render_widget(d1, chunks[0]); + + let question = Paragraph::new(center_vertically(chunks[1], &card.question)) + .style(match state.answer { + Answer::Write => { + if state.input == "" { + Style::default().fg(Color::Yellow) + } else { + Style::default() + } + } + _ => Style::default(), + }) + .alignment(Alignment::Center); + f.render_widget(question, chunks[1]); + + let answer = Paragraph::new(center_vertically(chunks[2], &state.input)) + .style(match state.answer { + Answer::Write => Style::default(), + Answer::Difficulty { difficulty: _ } => { + if is_correct(&state.input, &card.responses) { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::Red) + } + } + }) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL).title("Réponse")) + .wrap(Wrap { trim: true }); + f.render_widget(answer, chunks[2]); + + match state.answer { + Answer::Difficulty { + difficulty: selected, + } => { + if !is_correct(&state.input, &card.responses) || card.responses.len() > 1 { + let paragraph = Paragraph::new(center_vertically( + chunks[3], + &serialization::words_to_line(&card.responses), + )) + .alignment(Alignment::Center); + f.render_widget(paragraph, chunks[3]); + }; + + let difficulties = card.state.difficulties(); + let l = difficulties.len(); + let sep = Span::styled(" • ", Style::default()); + let tabs = difficulties + .iter() + .enumerate() + .map(|(i, d)| { + let style = if *d == selected { + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::UNDERLINED) + } else { + Style::default().add_modifier(Modifier::DIM) + }; + let d = Span::styled(difficulty::label(*d), style); + if i < l - 1 { + [d, sep.clone()].to_vec() + } else { + [d].to_vec() + } + }) + .collect::<Vec<Vec<Span>>>() + .concat(); + let p = + Paragraph::new(Text::from(Spans::from(tabs))).alignment(Alignment::Center); + f.render_widget(p, chunks[4]); + } + _ => {} + } + })?; + + if let Event::Input(key) = events.next()? { + match state.answer { + Answer::Write => match key { + Key::Char('\n') => { + let difficulty = if is_correct(&state.input, &card.responses) { + Difficulty::Good + } else { + Difficulty::Again + }; + state.answer = Answer::Difficulty { difficulty } + } + Key::Char(c) => { + state.input.push(c); + } + Key::Backspace => { + state.input.pop(); + } + _ => {} + }, + Answer::Difficulty { + difficulty: selected, + } => match key { + Key::Left => { + for d in relative_element(&card.state.difficulties(), &selected, -1).iter() + { + state.answer = Answer::Difficulty { difficulty: *d } + } + } + Key::Right => { + for d in relative_element(&card.state.difficulties(), &selected, 1).iter() { + state.answer = Answer::Difficulty { difficulty: *d } + } + } + Key::Char('\n') => return Ok(selected), + _ => {} + }, + } + } + } +} + +fn center_vertically(chunk: Rect, text: &String) -> String { + let text_lines = text.lines().count(); + let chunk_inner_lines: usize = (chunk.height - 2).into(); + let blank_lines = chunk_inner_lines - text_lines; + let newlines = "\n".repeat(blank_lines / 2); + format!("{}{}", newlines, text) +} + +fn is_correct(input: &String, responses: &Vec<String>) -> bool { + responses.contains(input) +} + +fn relative_element<T: Clone + PartialEq>(xs: &Vec<T>, x: &T, ri: i32) -> Option<T> { + let i = xs.iter().position(|t| t == x)? as i32 + ri; + if i >= 0 && i < xs.len() as i32 { + Some(xs[i as usize].clone()) + } else { + None + } +} |