aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--src/db/db.rs44
-rw-r--r--src/deck.rs6
-rw-r--r--src/gui/gui.rs78
-rw-r--r--src/gui/message.rs21
-rw-r--r--src/main.rs14
-rw-r--r--src/space_repetition.rs6
-rw-r--r--src/util/time.rs28
8 files changed, 151 insertions, 49 deletions
diff --git a/README.md b/README.md
index 363a67d..3a9f7d0 100644
--- a/README.md
+++ b/README.md
@@ -23,8 +23,7 @@ Cards are created from a plain text `./deck` file:
# TODO
-- Look at last modification of the deck, and omit db sync if last sync was after (+ show a message when synchronizing the deck)
-- Fix crashes on zoom / changing size
+- Fix crashes on zoom / changing vertical size
## Maybe
diff --git a/src/db/db.rs b/src/db/db.rs
index 30ea1da..b42da3f 100644
--- a/src/db/db.rs
+++ b/src/db/db.rs
@@ -19,13 +19,23 @@ pub fn init(database: String) -> Result<Connection> {
Ok(conn)
}
+pub fn last_deck_read(conn: &Connection) -> Option<u64> {
+ let mut stmt = conn
+ .prepare("SELECT deck_read FROM cards ORDER BY deck_read DESC LIMIT 1")
+ .ok()?;
+
+ let mut rows = stmt.query([]).ok()?;
+ let row = rows.next().ok()??;
+ row.get(0).ok()?
+}
+
/// Synchronize the DB with the deck:
///
/// - insert new cards,
/// - 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 = time::now()?;
+ let now = time::seconds_since_unix_epoch()?;
let state = serde_json::to_string(&space_repetition::init())?;
@@ -76,19 +86,21 @@ fn delete_read_before(conn: &Connection, t: u64) -> Result<()> {
}
pub fn pick_random_ready(conn: &Connection) -> Option<Card> {
+ let now = time::seconds_since_unix_epoch().ok()?;
+
let mut stmt = conn
.prepare(
"
SELECT question, responses, state, ready
FROM cards
- WHERE deleted IS NULL
+ WHERE deleted IS NULL AND ready <= ?
ORDER BY RANDOM()
LIMIT 1
",
)
.ok()?;
- let mut rows = stmt.query([]).ok()?;
+ let mut rows = stmt.query([now]).ok()?;
let row = rows.next().ok()??;
let state_str: String = row.get(2).ok()?;
let responses_str: String = row.get(1).ok()?;
@@ -101,9 +113,29 @@ pub fn pick_random_ready(conn: &Connection) -> Option<Card> {
})
}
+pub fn next_ready(conn: &Connection) -> Option<u64> {
+ let mut stmt = conn
+ .prepare(
+ "
+ SELECT ready
+ FROM cards
+ WHERE deleted IS NULL
+ ORDER BY ready
+ LIMIT 1
+ ",
+ )
+ .ok()?;
+
+ let mut rows = stmt.query([]).ok()?;
+ let row = rows.next().ok()??;
+ row.get(0).ok()?
+}
+
pub fn count_available(conn: &Connection) -> Option<i32> {
- let now = time::now().ok()?;
- let mut stmt = conn.prepare("SELECT COUNT(*) FROM cards WHERE ready <= ? AND deleted IS NULL").ok()?;
+ let now = time::seconds_since_unix_epoch().ok()?;
+ let mut stmt = conn
+ .prepare("SELECT COUNT(*) FROM cards WHERE ready <= ? AND deleted IS NULL")
+ .ok()?;
let mut rows = stmt.query([now]).ok()?;
let row = rows.next().ok()??;
@@ -111,7 +143,7 @@ pub fn count_available(conn: &Connection) -> Option<i32> {
}
pub fn update(conn: &Connection, question: &String, state: &space_repetition::State) -> Result<()> {
- let now = time::now()?;
+ let now = time::seconds_since_unix_epoch()?;
let ready = now + state.get_interval_seconds();
let state_str = serde_json::to_string(state)?;
diff --git a/src/deck.rs b/src/deck.rs
index 3524c96..e0f9fab 100644
--- a/src/deck.rs
+++ b/src/deck.rs
@@ -23,7 +23,7 @@ impl std::error::Error for ParseError {
}
}
-pub fn read(deck: &String) -> Result<Vec<Entry>> {
+pub fn read(deck: &str) -> Result<Vec<Entry>> {
let file = File::open(deck)?;
let reader = BufReader::new(file);
let mut entries: Vec<Entry> = Vec::new();
@@ -69,7 +69,9 @@ pub fn read(deck: &String) -> Result<Vec<Entry>> {
}
pub fn pp_from_path(path: &String) -> Option<String> {
- Some(capitalize(Path::new(&path).with_extension("").file_name()?.to_str()?))
+ Some(capitalize(
+ Path::new(&path).with_extension("").file_name()?.to_str()?,
+ ))
}
fn capitalize(s: &str) -> String {
diff --git a/src/gui/gui.rs b/src/gui/gui.rs
index 2f41a0b..2379cfb 100644
--- a/src/gui/gui.rs
+++ b/src/gui/gui.rs
@@ -1,28 +1,58 @@
+use crate::deck;
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 std::{fs, io, time::Duration};
+use termion::{raw::IntoRawMode, raw::RawTerminal, screen::AlternateScreen};
use tui::{backend::TermionBackend, Terminal};
-pub fn start(conn: &Connection, deck_name: &String) -> Result<()> {
+type Term = Terminal<TermionBackend<AlternateScreen<RawTerminal<io::Stdout>>>>;
+
+pub fn terminal() -> Result<Term> {
let stdout = io::stdout().into_raw_mode()?;
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
- let mut terminal = Terminal::new(backend)?;
+ Ok(Terminal::new(backend)?)
+}
+
+pub fn synchronize(
+ conn: &Connection,
+ term: &mut Term,
+ events: &Events,
+ deck_path: &str,
+ deck_name: &str,
+) -> Result<()> {
+ let last_modified = time::seconds_since_unix_epoch_of(fs::metadata(deck_path)?.modified()?)?;
+ let last_deck_read = db::last_deck_read(&conn);
+ let must_synchronize = last_deck_read.map(|r| r < last_modified).unwrap_or(true);
- let events = Events::new();
+ if must_synchronize {
+ let _ = message::show(term, events, &deck_name, "Synchronization du deck", false);
+ time::wait_at_least(
+ || db::synchronize(&conn, deck::read(&deck_path)?),
+ Duration::from_secs(1),
+ )?;
+ }
+ Ok(())
+}
+
+pub fn start(
+ conn: &Connection,
+ term: &mut Term,
+ events: &Events,
+ deck_name: &String,
+) -> Result<()> {
let mut answers = 0;
loop {
- let now = time::now()?;
+ let now = time::seconds_since_unix_epoch()?;
let title = title(deck_name, answers, db::count_available(&conn).unwrap_or(0));
match db::pick_random_ready(&conn) {
- Some(card) if card.ready <= now => {
- let difficulty = question::ask(&mut terminal, &events, &title, &card)?;
+ Some(card) => {
+ let difficulty = question::ask(term, events, &title, &card)?;
answers += 1;
db::update(
&conn,
@@ -30,17 +60,15 @@ pub fn start(conn: &Connection, deck_name: &String) -> Result<()> {
&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, &title, &message);
- break;
- }
None => {
- let message = format!("Aucune carte n’est disponible. Votre deck est-il vide ?");
- let _ = message::show(&mut terminal, &events, &title, &message);
+ let message = match db::next_ready(&conn) {
+ Some(ready) => format!(
+ "Prochaine carte disponible dans {}.",
+ time::pp_duration(ready - now)
+ ),
+ None => format!("Aucune carte n’est disponible. Votre deck est-il vide ?"),
+ };
+ let _ = message::show(term, events, &title, &message, true);
break;
}
}
@@ -53,8 +81,18 @@ fn title(deck_name: &String, answers: i32, available_cards: i32) -> String {
if answers == 0 && available_cards == 0 {
deck_name.to_string()
} else if available_cards == 0 {
- format!("{} ({} / {})", deck_name, answers, answers + available_cards)
+ format!(
+ "{} ({} / {})",
+ deck_name,
+ answers,
+ answers + available_cards
+ )
} else {
- format!("{} ({} / {})", deck_name, answers + 1, answers + available_cards)
+ format!(
+ "{} ({} / {})",
+ deck_name,
+ answers + 1,
+ answers + available_cards
+ )
}
}
diff --git a/src/gui/message.rs b/src/gui/message.rs
index 01d124e..28a1d2c 100644
--- a/src/gui/message.rs
+++ b/src/gui/message.rs
@@ -12,8 +12,9 @@ use tui::{
pub fn show<B: Backend>(
terminal: &mut Terminal<B>,
events: &Events,
- title: &String,
- message: &String,
+ title: &str,
+ message: &str,
+ wait: bool,
) -> Result<()> {
loop {
terminal.draw(|f| {
@@ -26,16 +27,22 @@ pub fn show<B: Backend>(
let d1 = util::title(title);
f.render_widget(d1, chunks[0]);
- let message = Paragraph::new(util::center_vertically(chunks[1], &message))
+ let message = Paragraph::new(util::center_vertically(chunks[1], &message.to_string()))
.alignment(Alignment::Center);
f.render_widget(message, chunks[1]);
})?;
- if let Event::Input(key) = events.next()? {
- match key {
- Key::Char('q') => return Ok(()),
- _ => (),
+ if wait {
+ if let Event::Input(key) = events.next()? {
+ match key {
+ Key::Char('q') => break,
+ _ => (),
+ }
}
+ } else {
+ break;
}
}
+
+ Ok(())
}
diff --git a/src/main.rs b/src/main.rs
index 44def4e..3e3e741 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ mod model;
mod space_repetition;
mod util;
+use crate::util::event::Events;
use anyhow::Result;
use std::path::PathBuf;
use structopt::StructOpt;
@@ -17,12 +18,13 @@ struct Opt {
}
fn main() -> Result<()> {
- let deck = Opt::from_args().deck;
- let conn = db::db::init(db_path(&deck))?;
- let entries = deck::read(&deck)?;
- db::db::synchronize(&conn, entries)?;
- let deck_name = deck::pp_from_path(&deck).unwrap_or("Deck".to_string());
- gui::gui::start(&conn, &deck_name)
+ let deck_path = Opt::from_args().deck;
+ let conn = db::db::init(db_path(&deck_path))?;
+ let deck_name = deck::pp_from_path(&deck_path).unwrap_or("Deck".to_string());
+ let mut term = gui::gui::terminal()?;
+ let events = Events::new();
+ gui::gui::synchronize(&conn, &mut term, &events, &deck_path, &deck_name)?;
+ gui::gui::start(&conn, &mut term, &events, &deck_name)
}
fn db_path(deck_path: &String) -> String {
diff --git a/src/space_repetition.rs b/src/space_repetition.rs
index 25cae7f..e2ab382 100644
--- a/src/space_repetition.rs
+++ b/src/space_repetition.rs
@@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
// Learning
const LEARNING_INTERVALS: [f32; 2] = [
- 1.0 / 60.0 / 24.0, // 1 minute
- 10.0 / 60.0 / 24.0, // 10 minutes
+ 1.0 / 60.0 / 24.0, // 1 minute (in days)
+ 10.0 / 60.0 / 24.0, // 10 minutes (in days)
];
// Ease
@@ -30,7 +30,7 @@ const INTERVAL_EASY_MUL: f32 = 1.3;
// Relearning
const RELEARNING_INTERVALS: [f32; 1] = [
- 10.0 / 60.0 / 24.0, // 10 minutes
+ 10.0 / 60.0 / 24.0, // 10 minutes (in days)
];
#[derive(Debug, PartialEq, Deserialize, Serialize)]
diff --git a/src/util/time.rs b/src/util/time.rs
index f88955d..d9a9f72 100644
--- a/src/util/time.rs
+++ b/src/util/time.rs
@@ -1,9 +1,14 @@
use anyhow::Result;
+use std::thread;
use std::time::SystemTime;
-pub fn now() -> Result<u64> {
- Ok(SystemTime::now()
- .duration_since(SystemTime::UNIX_EPOCH)?
+pub fn seconds_since_unix_epoch() -> Result<u64> {
+ Ok(seconds_since_unix_epoch_of(SystemTime::now())?)
+}
+
+pub fn seconds_since_unix_epoch_of(time: SystemTime) -> Result<u64> {
+ Ok(time
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)?
.as_secs())
}
@@ -31,3 +36,20 @@ fn plural(n: u64, str: &str) -> String {
format!("{} {}s", n, str)
}
}
+
+/// Call the function, then sleep if necessary.
+///
+/// Calling this will at least take the duration asked for in parameters.
+pub fn wait_at_least<F>(f: F, d: std::time::Duration) -> Result<()>
+where
+ F: Fn() -> Result<()>,
+{
+ let t1 = SystemTime::now();
+ f()?;
+ let t2 = SystemTime::now();
+ let elapsed = t2.duration_since(t1)?;
+ if elapsed < d {
+ thread::sleep(d - elapsed);
+ }
+ Ok(())
+}