diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/db/db.rs | 44 | ||||
| -rw-r--r-- | src/deck.rs | 6 | ||||
| -rw-r--r-- | src/gui/gui.rs | 78 | ||||
| -rw-r--r-- | src/gui/message.rs | 21 | ||||
| -rw-r--r-- | src/main.rs | 14 | ||||
| -rw-r--r-- | src/space_repetition.rs | 6 | ||||
| -rw-r--r-- | src/util/time.rs | 28 | 
7 files changed, 150 insertions, 47 deletions
| 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(()) +} | 
