diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | src/db/db.rs | 24 | ||||
-rw-r--r-- | src/deck.rs | 15 | ||||
-rw-r--r-- | src/gui/gui.rs | 24 | ||||
-rw-r--r-- | src/gui/message.rs | 41 | ||||
-rw-r--r-- | src/gui/mod.rs | 2 | ||||
-rw-r--r-- | src/gui/question.rs | 27 | ||||
-rw-r--r-- | src/gui/util.rs | 21 | ||||
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/model/card.rs | 1 | ||||
-rw-r--r-- | src/util/mod.rs | 1 | ||||
-rw-r--r-- | src/util/time.rs | 33 |
13 files changed, 154 insertions, 46 deletions
@@ -1,2 +1,3 @@ target/ -deck* +deck +deck.db @@ -23,6 +23,7 @@ Cards are created from a plain text `./deck` file: # TODO +- Show current iteration and number of available cards in the session (Deck 3 / 15) - Look at last modification of the deck, and omit db sync if last sync was after - Fix crashes on zoom / changing size diff --git a/src/db/db.rs b/src/db/db.rs index 9ed88ce..4c2d9a2 100644 --- a/src/db/db.rs +++ b/src/db/db.rs @@ -7,7 +7,8 @@ use anyhow::Result; use rand::{rngs::ThreadRng, Rng}; use rusqlite::{params, Connection}; use rusqlite_migration::{Migrations, M}; -use std::time::SystemTime; + +use crate::util::time; pub fn init(database: String) -> Result<Connection> { let mut conn = Connection::open(database)?; @@ -25,7 +26,7 @@ pub fn init(database: String) -> Result<Connection> { /// - keep existing cards, /// - hide unused cards (keep state in case the card is added back afterward). pub fn synchronize(conn: &Connection, entries: Vec<Entry>) -> Result<()> { - let now = get_current_time()?; + let now = time::now()?; let state = serde_json::to_string(&space_repetition::init())?; let mut rng = rand::thread_rng(); @@ -81,22 +82,20 @@ fn delete_read_before(conn: &Connection, t: u64) -> Result<()> { Ok(()) } -pub fn pick_ready(conn: &Connection) -> Option<Card> { - let now = get_current_time().ok()?; - +pub fn next_ready(conn: &Connection) -> Option<Card> { let mut stmt = conn .prepare( " - SELECT question, responses, state + SELECT question, responses, state, ready FROM cards - WHERE ready <= ? AND deleted IS NULL + WHERE deleted IS NULL ORDER BY ready LIMIT 1 ", ) .ok()?; - let mut rows = stmt.query([now]).ok()?; + let mut rows = stmt.query([]).ok()?; let row = rows.next().ok()??; let state_str: String = row.get(2).ok()?; let responses_str: String = row.get(1).ok()?; @@ -105,11 +104,12 @@ pub fn pick_ready(conn: &Connection) -> Option<Card> { question: row.get(0).ok()?, responses: serialization::line_to_words(&responses_str), state: serde_json::from_str(&state_str).ok()?, + ready: row.get(3).ok()?, }) } pub fn update(conn: &Connection, question: &String, state: &space_repetition::State) -> Result<()> { - let now = get_current_time()?; + let now = time::now()?; let ready = now + state.get_interval_seconds(); let state_str = serde_json::to_string(state)?; @@ -124,9 +124,3 @@ pub fn update(conn: &Connection, question: &String, state: &space_repetition::St Ok(()) } - -fn get_current_time() -> Result<u64> { - Ok(SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs()) -} diff --git a/src/deck.rs b/src/deck.rs index ce20ee9..af322c6 100644 --- a/src/deck.rs +++ b/src/deck.rs @@ -3,6 +3,7 @@ use anyhow::{Error, Result}; use std::fmt; use std::fs::File; use std::io::{prelude::*, BufReader}; +use std::path::Path; #[derive(Debug, Clone)] struct ParseError { @@ -22,7 +23,7 @@ impl std::error::Error for ParseError { } } -pub fn read(deck: String) -> Result<Vec<Entry>> { +pub fn read(deck: &String) -> Result<Vec<Entry>> { let file = File::open(deck)?; let reader = BufReader::new(file); let mut entries: Vec<Entry> = Vec::new(); @@ -66,3 +67,15 @@ pub fn read(deck: String) -> Result<Vec<Entry>> { Ok(entries) } + +pub fn pp_from_path(path: &String) -> Option<String> { + Some(capitalize(Path::new(&path).file_name()?.to_str()?)) +} + +fn capitalize(s: &str) -> String { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::<String>() + c.as_str(), + } +} diff --git a/src/gui/gui.rs b/src/gui/gui.rs index fdf686b..88333bb 100644 --- a/src/gui/gui.rs +++ b/src/gui/gui.rs @@ -1,11 +1,12 @@ -use crate::{db::db, gui::question, space_repetition, util::event::Events}; +use crate::util::time; +use crate::{db::db, gui::message, 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<()> { +pub fn start(conn: &Connection, deck_name: &String) -> Result<()> { let stdout = io::stdout().into_raw_mode()?; let stdout = AlternateScreen::from(stdout); let backend = TermionBackend::new(stdout); @@ -14,17 +15,28 @@ pub fn start(conn: &Connection) -> Result<()> { let events = Events::new(); loop { - match db::pick_ready(&conn) { - Some(card) => { - let difficulty = - question::ask(&mut terminal, &events, &card, "German".to_string())?; + let now = time::now()?; + + match db::next_ready(&conn) { + Some(card) if card.ready <= now => { + let difficulty = question::ask(&mut terminal, &events, &card, deck_name)?; db::update( &conn, &card.question, &space_repetition::update(card.state, difficulty), )?; } + Some(card) => { + let message = format!( + "Prochaine carte disponible dans {}.", + time::pp_duration(card.ready - now) + ); + let _ = message::show(&mut terminal, &events, &message, deck_name); + break; + } None => { + let message = format!("Aucune carte n’est disponible. Votre deck est-il vide ?"); + let _ = message::show(&mut terminal, &events, &message, deck_name); break; } } diff --git a/src/gui/message.rs b/src/gui/message.rs new file mode 100644 index 0000000..158416e --- /dev/null +++ b/src/gui/message.rs @@ -0,0 +1,41 @@ +use crate::gui::util; +use crate::util::event::{Event, Events}; +use anyhow::Result; +use termion::event::Key; +use tui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout}, + widgets::Paragraph, + Terminal, +}; + +pub fn show<B: Backend>( + terminal: &mut Terminal<B>, + events: &Events, + message: &String, + deck_name: &String, +) -> Result<()> { + loop { + terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([Constraint::Length(1), Constraint::Percentage(50)].as_ref()) + .split(f.size()); + + let d1 = util::title(deck_name); + f.render_widget(d1, chunks[0]); + + let message = Paragraph::new(util::center_vertically(chunks[1], &message)) + .alignment(Alignment::Center); + f.render_widget(message, chunks[1]); + })?; + + if let Event::Input(key) = events.next()? { + match key { + Key::Char('q') => return Ok(()), + _ => (), + } + } + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index cbf9675..f351eba 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,2 +1,4 @@ pub mod gui; +pub mod message; pub mod question; +pub mod util; diff --git a/src/gui/question.rs b/src/gui/question.rs index 64589d9..e88d6e5 100644 --- a/src/gui/question.rs +++ b/src/gui/question.rs @@ -1,4 +1,5 @@ use crate::{ + gui::util, model::{card::Card, difficulty, difficulty::Difficulty}, util::event::{Event, Events}, util::serialization, @@ -7,7 +8,7 @@ use anyhow::Result; use termion::event::Key; use tui::{ backend::Backend, - layout::{Alignment, Constraint, Direction, Layout, Rect}, + layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans, Text}, widgets::{Block, Borders, Paragraph, Wrap}, @@ -28,7 +29,7 @@ pub fn ask<B: Backend>( terminal: &mut Terminal<B>, events: &Events, card: &Card, - deck: String, + deck_name: &String, ) -> Result<Difficulty> { let mut state = State { input: String::new(), @@ -52,16 +53,10 @@ pub fn ask<B: Backend>( ) .split(f.size()); - let d1 = Paragraph::new(format!("{}", deck)) - .alignment(Alignment::Center) - .style( - Style::default() - .fg(Color::Blue) - .add_modifier(Modifier::BOLD), - ); + let d1 = util::title(deck_name); f.render_widget(d1, chunks[0]); - let question = Paragraph::new(center_vertically(chunks[1], &card.question)) + let question = Paragraph::new(util::center_vertically(chunks[1], &card.question)) .style(match state.answer { Answer::Write => { if state.input == "" { @@ -75,7 +70,7 @@ pub fn ask<B: Backend>( .alignment(Alignment::Center); f.render_widget(question, chunks[1]); - let answer = Paragraph::new(center_vertically(chunks[2], &state.input)) + let answer = Paragraph::new(util::center_vertically(chunks[2], &state.input)) .style(match state.answer { Answer::Write => Style::default(), Answer::Difficulty { difficulty: _ } => { @@ -96,7 +91,7 @@ pub fn ask<B: Backend>( difficulty: selected, } => { if !is_correct(&state.input, &card.responses) || card.responses.len() > 1 { - let paragraph = Paragraph::new(center_vertically( + let paragraph = Paragraph::new(util::center_vertically( chunks[3], &serialization::words_to_line(&card.responses), )) @@ -176,14 +171,6 @@ pub fn ask<B: Backend>( } } -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 .iter() diff --git a/src/gui/util.rs b/src/gui/util.rs new file mode 100644 index 0000000..38ed1e7 --- /dev/null +++ b/src/gui/util.rs @@ -0,0 +1,21 @@ +use tui::{ + layout::{Alignment, Rect}, + style::{Color, Modifier, Style}, + widgets::Paragraph, +}; + +pub fn title(str: &str) -> Paragraph { + Paragraph::new(str).alignment(Alignment::Center).style( + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ) +} + +pub 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) +} diff --git a/src/main.rs b/src/main.rs index 9720370..32d0be5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,8 @@ mod space_repetition; mod util; use anyhow::Result; -use structopt::StructOpt; use std::path::PathBuf; +use structopt::StructOpt; #[derive(StructOpt)] #[structopt()] @@ -19,9 +19,10 @@ struct Opt { fn main() -> Result<()> { let deck = Opt::from_args().deck; let conn = db::db::init(db_path(&deck))?; - let entries = deck::read(deck)?; + let entries = deck::read(&deck)?; db::db::synchronize(&conn, entries)?; - gui::gui::start(&conn) + let deck_name = deck::pp_from_path(&deck).unwrap_or("Deck".to_string()); + gui::gui::start(&conn, &deck_name) } fn db_path(deck_path: &String) -> String { diff --git a/src/model/card.rs b/src/model/card.rs index 3ac395e..811f877 100644 --- a/src/model/card.rs +++ b/src/model/card.rs @@ -5,4 +5,5 @@ pub struct Card { pub question: String, pub responses: Vec<String>, pub state: space_repetition::State, + pub ready: u64, } diff --git a/src/util/mod.rs b/src/util/mod.rs index c5504af..c866e61 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ #[allow(dead_code)] pub mod event; pub mod serialization; +pub mod time; diff --git a/src/util/time.rs b/src/util/time.rs new file mode 100644 index 0000000..f88955d --- /dev/null +++ b/src/util/time.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use std::time::SystemTime; + +pub fn now() -> Result<u64> { + Ok(SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs()) +} + +/// Pretty print duration. +pub fn pp_duration(seconds: u64) -> String { + let minutes = (seconds as f64 / 60.).round(); + let hours = (minutes / 60.).round(); + let days = (hours / 24.).round(); + + if seconds < 60 { + plural(seconds, "seconde") + } else if minutes < 60. { + plural(minutes as u64, "minute") + } else if hours < 24. { + plural(hours as u64, "heure") + } else { + plural(days as u64, "jour") + } +} + +fn plural(n: u64, str: &str) -> String { + if n <= 1 { + format!("{} {}", n, str) + } else { + format!("{} {}s", n, str) + } +} |