From 8a29f30fb2a949c03b318c4f7699136a8001be37 Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 13 Feb 2022 12:17:00 +0100 Subject: 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. --- README.md | 3 +- src/db/db.rs | 44 ++++++++++++++++++++++++---- src/deck.rs | 6 ++-- src/gui/gui.rs | 78 ++++++++++++++++++++++++++++++++++++------------- src/gui/message.rs | 21 ++++++++----- src/main.rs | 14 +++++---- src/space_repetition.rs | 6 ++-- src/util/time.rs | 28 ++++++++++++++++-- 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 { Ok(conn) } +pub fn last_deck_read(conn: &Connection) -> Option { + 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) -> 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 { + 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 { }) } +pub fn next_ready(conn: &Connection) -> Option { + 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 { - 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 { } 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> { +pub fn read(deck: &str) -> Result> { let file = File::open(deck)?; let reader = BufReader::new(file); let mut entries: Vec = Vec::new(); @@ -69,7 +69,9 @@ pub fn read(deck: &String) -> Result> { } pub fn pp_from_path(path: &String) -> Option { - 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>>>; + +pub fn terminal() -> Result { 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( terminal: &mut Terminal, events: &Events, - title: &String, - message: &String, + title: &str, + message: &str, + wait: bool, ) -> Result<()> { loop { terminal.draw(|f| { @@ -26,16 +27,22 @@ pub fn show( 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 { - Ok(SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH)? +pub fn seconds_since_unix_epoch() -> Result { + Ok(seconds_since_unix_epoch_of(SystemTime::now())?) +} + +pub fn seconds_since_unix_epoch_of(time: SystemTime) -> Result { + 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, 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(()) +} -- cgit v1.2.3