aboutsummaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorJoris2021-11-14 23:25:55 +0100
committerJoris2021-11-19 11:42:20 +0100
commit9f94611a42d41cf94cdccb00b5d2eec0d5d02970 (patch)
tree9bab5bc342e22aa38b13a2dbd3525bbfe2beedb5 /src/gui
parent59c44b15010eea5490896a5b5d427b415ad6f56a (diff)
Add initial working version
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.rs34
-rw-r--r--src/gui/mod.rs2
-rw-r--r--src/gui/question.rs198
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
+ }
+}