Cardagain
Spaced repetition flashcards for handheld Linux devices, specifically the Anbernic RG35XXSP running Knulli.
What it is
Cardagain is an Anki-style flashcard app you can study on your handheld. It implements the SM-2 algorithm for scheduling reviews, so cards you struggle with come back sooner, and cards you know well wait longer.
Key features
- Study anywhere: Review flashcards on your RG35XXSP during commutes, breaks, or couch time
- SM-2 scheduling: Same spaced repetition algorithm as Anki — efficient, proven
- CSV deck import: Add decks by dropping
.csvfiles in the data folder - No sync needed: All data stays on the device (or SD card)
What it looks like
Main Menu Deck Select Study (Front) Study (Back)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Cardagain │ │ Japanese │ │ QUESTION │ │ QUESTION │
│ │ │ 5 due | 3 new│ │ │ │ Hello │
│ > Study │ │ │ │ Hello │ │ ──────────── │
│ Stats │ │ > French │ │ │ │ ANSWER │
│ Quit │ │ 2 due | 0 new│ │ Press A to │ │ こんにちは │
│ │ │ │ │ flip │ │ │
└──────────────┘ └──────────────┘ └──────────────┘ │ [Again][Good]│
│ [Hard][Easy] │
└──────────────┘
Supported devices
| Device | OS | Status |
|---|---|---|
| Anbernic RG35XXSP | Knulli | Primary target |
| Anbernic RG35XX H | Knulli | Should work |
| Anbernic RG35XX Plus | Knulli | Should work |
| Other Knulli/ArkOS devices | Any Linux | Unknown |
How it works
- Add decks: Drop
.csvfiles into the app’s data directory - Study daily: Open the app, select a deck, review due cards
- Rate yourself: Again (1 min), Hard (2-3 days), Good (default interval), Easy (longer)
- Come back tomorrow: The SM-2 algorithm schedules your next review
Next steps
- Quick Start: Get studying in 5 minutes
- Adding Decks: How to create and import flashcard decks
- Controls: Button reference for the RG35XXSP
Quick Start
Get Cardagain running on your RG35XXSP in a few minutes.
Install on Knulli
Option A: Tools Folder (Recommended — No PortMaster Required)
- Download
cardagain-0.1.0.zipfrom the releases page - Extract it to your SD card’s
roms/tools/directory so you have:/userdata/roms/tools/ └── cardagain/ # app folder ├── Cardagain.sh # launcher script ├── cardagain # binary ├── fonts/ # bundled font └── gameinfo.xml # EmulationStation metadata - On the device, press START → Game Settings → Update Gameslist
- Launch Cardagain from the Tools menu
Option B: PortMaster
- Download
cardagain-0.1.0.zipfrom the releases page - Copy it to your device’s
roms/ports/folder - Install via the PortMaster GUI
- Launch from the Ports menu
First launch
The app creates a demo Japanese deck automatically. You can study it immediately or add your own decks.
Study your first cards
- From the main menu, select Study
- Pick the Japanese deck
- You’ll see a card front, e.g. “Hello”
- Press A to flip and see the answer: “こんにちは”
- Rate yourself:
- A (Again) — You didn’t know it. Review in 1 minute.
- B (Good) — You got it. Standard interval.
- X (Hard) — You barely got it. Shorter interval.
- Y (Easy) — Too easy. Longer interval.
- Repeat until the session is complete
Updating
To update to a new version:
- Download the new release zip
- Extract it over your existing folder (tools/ or ports/)
- Your decks and reviews in
data.jsonare preserved - Press START → Game Settings → Update Gameslist
Check your version on the Stats screen.
Add your own deck
Create a .csv file on your computer:
# MyDeck.csv
front,back
Hello,こんにちは
Thank you,ありがとう
Good morning,おはよう
Copy it to the app’s data directory (/userdata/roms/tools/cardagain/ or /roms/ports/cardagain/, where data.json lives), then restart the app. The deck appears automatically.
See Adding Decks for full details.
Controls
RG35XXSP Button Mapping
| Button | Action |
|---|---|
| D-Pad Up/Down | Navigate menus, scroll lists |
| A | Confirm / Flip card / Rate Again |
| B | Back / Rate Good |
| X | Rate Hard |
| Y | Rate Easy |
| Menu | Quit application |
Context-specific actions
Main Menu
- D-Pad: Select between Study / Stats / Quit
- A: Confirm selection
Deck Select
- D-Pad: Navigate deck list
- A: Start studying selected deck
- B: Back to Main Menu
Study Session (Front of card)
- A: Flip card to show answer
- B: Back to deck list (abandon session)
Study Session (Back of card)
- A: Rate Again (failed, review soon)
- B: Rate Good (got it, normal interval)
- X: Rate Hard (barely got it, shorter interval)
- Y: Rate Easy (too easy, longer interval)
Stats Screen
- A or B: Back to Main Menu
macOS Development (Keyboard)
When running on macOS for testing:
| Key | Maps to |
|---|---|
| ↑ / ↓ | D-Pad |
| Space / Enter / Z | A button |
| Backspace / X | B button |
| C | X button |
| V | Y button |
| Escape | Menu / Quit |
Adding Decks
Cardagain imports decks by scanning its data directory for .csv and .json files on startup.
Where to place files
The data directory is the same folder the binary lives in. On Knulli it’s typically:
/userdata/roms/tools/cardagain/
CSV format (recommended)
The simplest way to add a deck. The filename becomes the deck name.
Example: Spanish.csv
# Lines starting with # are comments
Hello,Hola
Thank you,Gracias
Good morning,Buenos días
How are you?,¿Cómo estás?
Good night,Buenas noches
CSV rules
- Filename = deck name (without
.csv) - One card per line:
front,back - Comments: Lines starting with
#are ignored - Empty lines: Skipped automatically
- Duplicate names: If a deck with the same name already exists, the file is ignored
- Malformed lines: Lines without a comma are skipped
JSON format
You can also place individual deck files exported from the app (or hand-written) as .json files. The format matches the internal Deck structure.
How importing works
- Start the app
- It scans the data directory for
.csvand.jsonfiles - New decks are imported and immediately saved to
data.json - The deck appears in the Deck Select screen
Creating decks on your computer
Use any text editor or spreadsheet app. Save as .csv with UTF-8 encoding (important for non-Latin characters like Japanese or Arabic).
Tips for large decks
- One file per deck keeps things organized
- Use comments to group cards by topic
- Test with a small deck first (10-20 cards)
Example: Multi-topic deck
# Japanese Basics.csv
# Greetings
Hello,こんにちは
Good morning,おはよう
Thank you,ありがとう
# Numbers
One,一
Two,二
Three,三
# Common phrases
Where is the bathroom?,トイレはどこですか
I don't understand,わかりません
Studying
Cardagain uses the SM-2 algorithm (same as Anki) to schedule your reviews. The goal is to review a card right when you’re about to forget it — not too early (wastes time) and not too late (you’ve already forgotten).
Study session flow
- Select deck from the Deck Select screen
- Study screen shows the front of the first due card
- Press A to flip and reveal the answer
- Rate yourself with A/B/X/Y
- Next card appears automatically
- Session ends when all due cards are reviewed
Rating guide
| Button | Rating | Meaning | When to use |
|---|---|---|---|
| A | Again | Complete failure | You couldn’t recall the answer at all |
| X | Hard | Difficult recall | You got it but struggled; felt unsure |
| B | Good | Solid recall | You got it with normal effort (default) |
| Y | Easy | Instant recall | You knew it immediately; almost too easy |
How SM-2 schedules cards
The algorithm tracks each card’s easiness factor (EF), interval, and repetition count:
- Again: Resets the card to 1-minute review
- Hard: Increases interval slightly, lowers EF
- Good: Standard interval increase (EF × current interval)
- Easy: Larger interval increase, raises EF
New cards start with a 1-day interval, then 6 days if you get them right twice. From there, intervals grow multiplicatively based on your EF.
Session tips
- Study daily if possible — consistency beats intensity
- Be honest with ratings — the algorithm only works with accurate data
- It’s okay to fail — “Again” is the most important button for learning
- Stop when tired — come back later; fatigue hurts retention
What “due” means
A card is “due” when its scheduled review date has passed. The app shows:
Japanese - 3/15
Meaning you’ve reviewed 3 of 15 due cards in this session.
New cards vs reviews
- New cards (never studied) are shown first
- Due reviews are sorted by overdue date
- Learning cards (recently failed) are mixed in appropriately
Data persistence
All reviews are saved to data.json immediately. If the app crashes or you quit mid-session, your progress up to the last completed card is preserved.
Building for RG35XXSP
You have three options to get a working binary onto your device.
Option A: Docker (Recommended for macOS)
This builds inside a Linux aarch64 container, avoiding cross-compilation issues.
just docker-aarch64
just package # Create tools/ folder zip (default)
# or:
just package-portmaster # Create PortMaster zip
Requirements: Docker Desktop installed.
Option B: Native Linux / WSL2
If you have a Linux environment with the aarch64 cross-compiler:
just install-deps-debian # apt install libsdl2-dev libsdl2-ttf-dev
just install-cross-debian # apt install gcc-aarch64-linux-gnu
just aarch64 # Cross-compile
just package # Create tools/ folder zip
WSL2 with Debian or Ubuntu is the most reliable native setup.
Option C: Build Directly on the Device
The RG35XXSP runs Linux. If you can SSH into it:
# On the device
sudo apt install libsdl2-dev libsdl2-ttf-dev git curl
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
git clone <repo>
cd cardagain
cargo build --release
# Copy binary to the tools directory
mkdir -p /userdata/roms/tools/cardagain/
cp target/release/cardagain /userdata/roms/tools/cardagain/
# Download an open-source font
wget https://github.com/dejavu-fonts/dejavu-fonts/raw/master/ttf/DejaVuSans.ttf
mkdir -p /userdata/roms/tools/cardagain/fonts/
cp DejaVuSans.ttf /userdata/roms/tools/cardagain/fonts/
This avoids ALL cross-compilation issues entirely.
Installing
Tools Folder (Default)
Extract the zip to /userdata/roms/tools/.
Required structure:
/userdata/roms/tools/
└── cardagain/ # app folder
├── Cardagain.sh # launcher (EmulationStation discovers this)
├── cardagain # binary
├── fonts/ # bundled font
└── gameinfo.xml # metadata
- Extract
cardagain-0.1.0.zipto/userdata/roms/tools/ - Press START → Game Settings → Update Gameslist
- Launch Cardagain from the Tools menu
PortMaster
- Copy
cardagain-0.1.0.ziptoroms/ports/ - Install via the PortMaster GUI
- Launch from the Ports menu
Updating
To update to a new version:
- Download the new
cardagain-X.Y.Z.zip - Extract it over your existing folder (tools/ or ports/)
- Your
data.json(decks, reviews) and any.csvdecks are preserved — they are not included in the release zip, so extracting won’t overwrite them - Press START → Game Settings → Update Gameslist
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| Not showing in Tools menu | Missing Cardagain.sh launcher | Ensure Cardagain.sh is inside cardagain/ folder |
| Crash on launch | Missing libSDL2_ttf | Install SDL2_ttf on the device |
| Black screen | Wrong working directory | Ensure launcher cd’s to the app folder |
| No controller input | SDL2 gamecontroller not initialized | Check launch.log in cardagain/ folder |
| Cards not importing | Wrong directory | Place .csv files in cardagain/ where data.json lives |
Architecture
Cardagain is a Rust application using SDL2 for rendering and input. It’s designed for simplicity and low resource usage on handheld devices.
Project structure
src/
├── main.rs # Entry point: SDL2 init, panic hooks, main loop
├── app.rs # Screen state machine (MainMenu → DeckSelect → Study → Stats)
├── data.rs # Card, Deck, Review models + SM-2 scheduler
├── import.rs # CSV/JSON deck import logic
├── input.rs # Keyboard + gamecontroller input mapping
├── render.rs # SDL2 canvas, font rendering, UI primitives
└── study.rs # Study session state machine (Front → Flip → Rate)
fonts/ # Bundled fonts (copied into release zip)
gameinfo.xml # EmulationStation metadata
scripts/
├── build-aarch64.sh # Cross-compile script
├── docker-build.sh # Docker-based aarch64 build
├── Dockerfile.build # Ubuntu 20.04 + SDL2 build image
└── package-tools.sh # Create release zip
book.toml # mdbook configuration
justfile # Build recipes (native, aarch64, package)
Data flow
Startup
→ Load data.json (or create demo deck)
→ Scan for .csv/.json deck files
→ Import new decks
→ Save data.json
Main loop (60 FPS)
→ Poll input (keyboard + controller)
→ Update app state
→ Render current screen
→ Sleep to cap at ~30 FPS (save battery)
Study session
→ Build due card queue (new first, then by due date)
→ Show front → wait for flip
→ Show back → wait for rating
→ Apply SM-2 schedule
→ Save after each rating
Key design decisions
- No allocations in hot path: Textures are recreated per frame (simple but not optimal; acceptable for a flashcard app)
- Immediate-mode UI: No widget tree; draw calls happen directly in
draw_*methods - Blocking save:
data.jsonis rewritten synchronously after each rating. Cards are small; this is fast enough - No threading: Single-threaded; SDL2 event pump drives everything
Screen state machine
[MainMenu] --A(Study)--> [DeckSelect] --A(start session)--> [Study]
B(back) |
|
[MainMenu] <----------------------B(abandon)----------------------|
| |
|----------------A(Stats)----------------> [Stats] <---------|
Memory usage
Typical footprint on RG35XXSP:
| Component | Size |
|---|---|
| Binary | ~500 KB |
| Heap (fonts + textures) | ~5-10 MB |
| Data (1000 cards) | ~200 KB JSON |
| Total | < 20 MB |
Well within the RG35XXSP’s 1GB RAM.
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.
Building
Prerequisites
macOS (development)
brew install sdl2 sdl2_ttf
Linux (Debian/Ubuntu)
sudo apt install libsdl2-dev libsdl2-ttf-dev
Build commands
# Native macOS build
just native
# Run locally
just run
# Cross-compile for RG35XXSP via Docker
just docker-aarch64
# Create release zip
just package
# Run tests
just test
# Check (fast type-check)
just check
Build profiles
Release
[profile.release]
opt-level = 3
lto = true
strip = true
panic = "abort"
lto = true: Link-time optimization reduces binary sizestrip = true: Remove debug symbolspanic = "abort": Smaller binary, no unwinding
Typical release binary: ~500 KB
Development
[profile.dev]
opt-level = 1
Fast compilation for iteration.
Cross-compilation details
Docker approach (recommended)
The Docker image (scripts/Dockerfile.build) is based on Ubuntu 20.04 for glibc compatibility. It builds:
- SDL2 2.26.2 from source (avoids macOS Metal symbol issues)
- SDL2_ttf 2.20.2 from source (includes bundled FreeType/HarfBuzz)
- Rust stable via rustup
Native Linux approach
Requires aarch64-linux-gnu-gcc toolchain and aarch64 SDL2 libraries:
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
sudo apt install libsdl2-dev:arm64 libsdl2-ttf-dev:arm64
The scripts/build-aarch64.sh script detects macOS and refuses to run (since macOS SDL2 libs are incompatible).
PortMaster Packaging
PortMaster is the distribution mechanism for homebrew on handheld Linux devices (Knulli, ArkOS, AmberELEC, etc.).
The portmaster/ folder
The portmaster/ folder in the project root contains the source files that get packaged for distribution. It’s separate from src/ (application code) because these files are metadata and scripts for the PortMaster ecosystem, not the app itself.
| File | Purpose | Consumed by |
|---|---|---|
Cardagain.sh | Launch script | PortMaster at runtime |
port.json | App metadata | PortMaster repository index |
gameinfo.xml | EmulationStation metadata | EmulationStation UI |
control.txt | Controller → keyboard mapping | gptokeyb at runtime |
README.md | PortMaster wiki docs | PortMaster GUI / website |
fonts/ | Bundled open-source fonts | The application at runtime |
The scripts/package-portmaster.sh script copies these files, adds the compiled binary, and creates the final zip.
Package structure
cardagain-0.1.0.zip
├── Cardagain.sh # Launch script (extracted to roms/ports/)
└── cardagain/ # Game directory
├── cardagain # ELF aarch64 binary
├── control.txt # gptokeyb controller mapping
├── port.json # PortMaster metadata
├── gameinfo.xml # EmulationStation metadata
├── README.md # PortMaster wiki docs
└── fonts/
└── DejaVuSans.ttf # Bundled font
Required files
Cardagain.sh
The launch script. PortMaster sets environment variables like $GAMEDIR, $CONFDIR, $GPTOKEYB before calling it.
Key responsibilities:
cdto the game directory- Set
XDG_DATA_HOMEfor save files - Run
gptokeybfor controller → keyboard mapping - Launch the binary
- Clean up on exit
port.json
PortMaster metadata for the repository index:
{
"version": 2,
"name": "cardagain.zip",
"items": ["Cardagain.sh", "cardagain"],
"attr": {
"title": "Cardagain",
"desc": "Spaced Repetition Flashcards",
"rtr": true,
"arch": ["aarch64", "armhf"]
}
}
gameinfo.xml
EmulationStation metadata:
<gameList>
<game>
<path>./Cardagain.sh</path>
<name>Cardagain</name>
<desc>...</desc>
<genre>puzzle,educational</genre>
</game>
</gameList>
control.txt
gptokeyb mapping file. Maps controller buttons to keyboard keys the game expects:
a = space # A button → Space key
b = x # B button → X key
x = c # X button → C key
y = v # Y button → V key
menu = esc # Menu → Escape
Packaging script
scripts/package-portmaster.sh creates the zip:
- Finds the aarch64 binary (or warns if using native)
- Stages files into a temp directory
- Bundles fonts from
portmaster/fonts/ - Creates the zip with
Cardagain.shat the root
Installation on device
- Copy
cardagain-0.1.0.ziptoroms/ports/ - Extract:
cd /roms/ports unzip cardagain-0.1.0.zip - Launch from EmulationStation: Ports → Cardagain
Submitting to PortMaster
To add to the official repository:
- Fork PortsMaster/PortMaster-New
- Add your files under
ports/cardagain/ - Submit a PR
The PortMaster team reviews and merges. Once accepted, users can install directly from the PortManager GUI.
ADR 0001: Rust + SDL2 for Handheld Development
Status
Accepted
Context
We need to build a flashcard app for the Anbernic RG35XXSP (aarch64 Linux, 1GB RAM, 640×480 screen). The device runs Knulli, a Batocera fork with SDL2 pre-installed for emulators.
Options considered:
| Option | Pros | Cons |
|---|---|---|
| Rust + SDL2 | Fast, small binaries, SDL2 already on device | Must cross-compile from macOS |
| Python + Pygame | Easy development | Large runtime, slow startup |
| C + SDL2 | Fastest, smallest | Manual memory management, slower development |
| Godot | Rich UI, visual editor | Too heavy for simple app, overkill |
| Love2D (Lua) | Simple, proven for games | Lua runtime overhead, less type safety |
Decision
Use Rust with SDL2.
Consequences
Positive
- Binary size: ~500 KB (vs Python’s ~50MB+ runtime)
- Startup time: Instant (vs Python’s import overhead)
- Memory usage: <20 MB total (fits comfortably in 1GB)
- SDL2 is already on Knulli — no additional runtime deps
- Type safety prevents common C bugs
Negative
- Cross-compilation from macOS requires Docker (SDL2 library format mismatch)
- Rust compile times are slower than Python
- Smaller ecosystem than Python for some tasks
Notes
The SDL2 window/renderer API is low-level but sufficient for a flashcard app. We don’t need GPU acceleration or complex UI widgets. The sdl2 crate provides safe bindings and sdl2_ttf handles text rendering.
ADR 0002: SM-2 Algorithm for Spaced Repetition
Status
Accepted
Context
We need a scheduling algorithm for flashcard reviews. The app targets users who want Anki-style spaced repetition on a handheld device.
Options considered:
| Algorithm | Pros | Cons |
|---|---|---|
| SM-2 (SuperMemo-2) | Proven, simple, same as Anki | Less sophisticated than newer algorithms |
| FSRS (Free Spaced Repetition Scheduler) | Machine learning, more accurate | Complex, needs training data, harder to implement |
| SM-17 (SuperMemo-17) | Most advanced | Proprietary, extremely complex |
| Custom interval | Simple | No optimization, poor retention |
| Leitner system | Simple, visual | Less efficient than SM-2 |
Decision
Use SM-2.
Consequences
Positive
- Same algorithm as Anki — users already understand it
- Simple to implement (~30 lines of Rust)
- No machine learning dependencies
- Well-studied, proven effective for decades
- Easy to debug and reason about
Negative
- Doesn’t adapt to individual user patterns as well as FSRS
- Fixed EF change formula; newer research suggests improvements
- No optimization for specific card types
Implementation
#![allow(unused)]
fn main() {
pub fn review(&mut self, quality: u8, now: DateTime<Utc>) {
let q = quality.clamp(0, 5);
if q < 3 {
self.repetitions = 0;
self.interval_days = 1;
} else {
self.repetitions += 1;
self.interval_days = match self.repetitions {
1 => 1,
2 => 6,
_ => (self.interval_days as f64 * self.easiness_factor).round() as i64,
};
}
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);
}
}
Future work
Could migrate to FSRS later if user demand exists. SM-2 data is compatible — we’d just recompute intervals with the new algorithm.
ADR 0003: CSV Deck Import
Status
Accepted
Context
Users need to add custom flashcard decks. The device has no keyboard, so in-app card creation is impractical. We need a simple way to bulk-import cards from a computer.
Options considered:
| Format | Pros | Cons |
|---|---|---|
| CSV | Universal, editable in any spreadsheet, simple | No metadata, flat structure |
| JSON | Matches internal format, supports metadata | Harder to edit manually |
| Anki .apkg | Direct Anki compatibility | Complex format, needs parser |
| TSV | Like CSV, no comma escaping issues | Less universal |
| Markdown tables | Human-readable | Harder to parse reliably |
Decision
Support CSV as primary, JSON as secondary.
Consequences
Positive
- CSV is editable in Excel, Google Sheets, Numbers, Vim, anything
- One file per deck keeps things organized
- Simple format:
front,back— no learning curve - Comments (
#) and empty lines are ignored for flexibility - Duplicate detection prevents accidental re-imports
Negative
- No support for card metadata (tags, images, audio)
- Commas in card text need careful escaping (we split on first comma)
- No deck hierarchy or subdecks
- Can’t import Anki decks directly
Format
# Lines starting with # are comments
Hello,こんにちは
Thank you,ありがとう
Good morning,おはよう
Rules:
- Filename = deck name (e.g.,
Japanese.csv→ deck “Japanese”) - One card per line:
front,back - First comma splits front from back (rest of line is back)
- UTF-8 encoding required for non-Latin characters
Future work
- Anki
.apkgimport (popular request, complex to implement) .tsvsupport (trivial addition if requested)- In-app card editor (for small corrections, not bulk creation)
ADR 0004: Docker for Cross-Compilation
Status
Accepted
Context
Primary development is on macOS (Apple Silicon), but the target device runs Linux aarch64. macOS cannot cross-compile SDL2 apps directly because macOS SDL2 libraries (.dylib, Mach-O format) are incompatible with the Linux aarch64 cross-compiler (needs .so, ELF format).
Options considered:
| Approach | Pros | Cons |
|---|---|---|
| Docker | Same environment every time, works on any host, reproducible | Slower first build (image creation) |
| WSL2 | Native Linux on Windows, can share files | Windows only, setup complexity |
| Virtual Machine | Full Linux environment | Heavy, slow, manual maintenance |
| Remote Linux server | Fast, always-on | Requires server, network dependency |
| Build on device | No cross-compilation at all | Slow compilation on H700, needs dev tools on device |
| nix-shell | Reproducible, declarative | Steep learning curve, nix adoption |
Decision
Use Docker as the primary cross-compilation method. Provide build-on-device as fallback.
Consequences
Positive
- Works on macOS, Linux, and Windows (with Docker Desktop)
- Container has exact SDL2 version we need (2.26.2)
- No host dependency contamination
- Reproducible builds
- Apple Silicon Macs run aarch64 Linux containers natively (no emulation)
Negative
- Docker image is ~2.5 GB (Ubuntu 20.04 + SDL2 build + Rust toolchain)
- First build takes ~2 minutes to create the image
- Container startup adds ~5 seconds per build
- Requires Docker Desktop installed
Implementation
scripts/Dockerfile.build creates the image:
FROM ubuntu:20.04
# Install build dependencies
RUN apt-get install -y build-essential libsdl2-dev libsdl2-ttf-dev ...
# Build SDL2 2.26.2 from source (avoids macOS Metal symbol issues)
RUN wget https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.26.2.tar.gz
...
# Build SDL2_ttf 2.20.2 from source
RUN wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-2.20.2.tar.gz
...
# Install Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
RUN rustup target add aarch64-unknown-linux-gnu
scripts/docker-build.sh runs the build:
docker build -t cardagain-build -f scripts/Dockerfile.build .
docker run --rm -v $(pwd):/workspace -w /workspace cardagain-build \
bash -c 'cargo build --release --target aarch64-unknown-linux-gnu'
Future work
- Multi-stage Dockerfile to reduce image size
- Cache Rust dependencies between builds
- Publish pre-built image to Docker Hub or GHCR