Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Data Model

Core types

Card

#![allow(unused)]
fn main() {
pub struct Card {
    pub id: u64,
    pub deck_id: u64,
    pub front: String,          // Question
    pub back: String,           // Answer
    pub created_at: DateTime<Utc>,
    pub sm2: Sm2State,
}
}

Sm2State

#![allow(unused)]
fn main() {
pub struct Sm2State {
    pub easiness_factor: f64,   // Starts at 2.5, adjusts per rating
    pub interval_days: i64,     // Current interval
    pub repetitions: u32,       // Consecutive successful reviews
    pub due_date: DateTime<Utc>,
    pub last_reviewed: Option<DateTime<Utc>>,
}
}

Deck

#![allow(unused)]
fn main() {
pub struct Deck {
    pub id: u64,
    pub name: String,
    pub created_at: DateTime<Utc>,
    pub cards: Vec<Card>,
    pub reviews: Vec<Review>,
}
}

Review

#![allow(unused)]
fn main() {
pub struct Review {
    pub card_id: u64,
    pub timestamp: DateTime<Utc>,
    pub quality: u8,            // 1=Again, 2=Hard, 3=Good, 4=Easy
    pub interval_scheduled: i64,
}
}

SM-2 algorithm

The review function adjusts EF and interval based on quality:

#![allow(unused)]
fn main() {
pub fn review(&mut self, quality: u8, now: DateTime<Utc>) {
    let q = quality.clamp(0, 5);

    if q < 3 {
        // Failed: reset to 1-day interval
        self.repetitions = 0;
        self.interval_days = 1;
    } else {
        // Success: increase interval
        self.repetitions += 1;
        self.interval_days = match self.repetitions {
            1 => 1,
            2 => 6,
            _ => (self.interval_days as f64 * self.easiness_factor).round() as i64,
        };
    }

    // Adjust easiness factor
    let ef_change = 0.1 - (5 - q) as f64 * (0.08 + (5 - q) as f64 * 0.02);
    self.easiness_factor = (self.easiness_factor + ef_change).max(1.3);

    self.due_date = now + Duration::days(self.interval_days);
    self.last_reviewed = Some(now);
}
}

Data persistence

All data is stored in a single JSON file (data.json):

{
  "decks": [
    {
      "id": 1,
      "name": "Japanese",
      "cards": [...],
      "reviews": [...]
    }
  ],
  "next_id": 2
}

Save strategy

  • Save after each rating during study
  • Save after importing decks on startup
  • Save on clean exit
  • Synchronous write (fine for small data; ~ms latency)

Data directory

The app uses XDG_DATA_HOME if set (some launchers provide it). Otherwise it resolves the data directory from the binary’s location, so data.json and deck files live next to the executable regardless of working directory. For development, it falls back to the current directory.