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.