aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/db/db.rs24
-rw-r--r--src/deck.rs15
-rw-r--r--src/gui/gui.rs24
-rw-r--r--src/gui/message.rs41
-rw-r--r--src/gui/mod.rs2
-rw-r--r--src/gui/question.rs27
-rw-r--r--src/gui/util.rs21
-rw-r--r--src/main.rs7
-rw-r--r--src/model/card.rs1
-rw-r--r--src/util/mod.rs1
-rw-r--r--src/util/time.rs33
11 files changed, 151 insertions, 45 deletions
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)
+ }
+}