aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoris2022-02-13 12:17:00 +0100
committerJoris2022-02-13 12:17:00 +0100
commit8a29f30fb2a949c03b318c4f7699136a8001be37 (patch)
tree51decc33aa776201bc800dc2196bc4f8b72337d7
parent8170fb5e432cc81986479a6a3a400e009426d76a (diff)
Synchronize deck only if necessary
Look at the modification time of the deck, and synchronize if it has been modified after the last deck read.
-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(())
+}