CLAUDE.md#
Guidance for Claude Code when working in this repository.
Commands#
cargo build # build everything
cargo check # check without artifacts
cargo test # all tests
cargo test -p tala-format card_count # single test
cargo run -p tala-cli -- check <deck-dir>
cargo run -p tala-cli -- review <deck-dir>
cargo run -p tala # GUI (uses cwd or ~/.config/tala/dir)
Architecture#
Cargo workspace, edition 2024. All crates under crates/.
tala-format → tala-srs → tala-typst → tala (GUI)
↑
tala-cli
tala-format#
Parses .typ card files using typst-syntax. Entry point: parse_cards(source: &str) -> Vec<CardEntry>.
Uses LinkedNode (not SyntaxNode) — LinkedNode::range() returns Range<usize>; SyntaxNode only exposes an opaque Span. Walker matches SyntaxKind::FuncCall, dispatches on function name.
Card syntax:
#card(tags: ("t1",), dir: "bi")[front][back] → CardKind::FrontBack
#cloze(tags: ("t1",))[body #blank[hidden] ...] → CardKind::Cloze
No #img_cloze — removed. Image region overlays live in the sidecar as RectEntry.
No id: field — cards are identified by position index in the file ("0", "1", ...).
dir defaults to Forward; "bi" → Bidirectional (two review items: forward + reverse).
Key types:
pub struct CardEntry {
pub tags: Vec<String>,
pub kind: CardKind,
pub span: Range<usize>, // byte range of the full #card(...)[...] call
}
pub enum CardKind {
FrontBack { front_span, back_span, dir: Direction },
Cloze { body_span, blanks: Vec<BlankEntry> },
}
pub struct BlankEntry { pub index: usize, pub span: Range<usize>, pub content_span: Range<usize> }
pub enum Direction { Forward, Bidirectional }
Span gotcha: the # sigil is outside the FuncCall AST node. card.span points to card(...) not #card(...). Use span.start - 1 when byte-splicing to include the sigil.
tala-srs#
Loads/saves <cards>.srs.json sidecar alongside <cards>.typ. All FSRS schedule data lives here, never in the typst source. Image rect overlays also live here.
Primary API:
Sidecar::load_or_empty_for(typ_path: &Path) -> Result<Self>
Sidecar::save_for(&self, typ_path: &Path) -> Result<()>
Sidecar::path_for(typ_path: &Path) -> PathBuf // derives .srs.json path
Sidecar::empty() -> Self
sidecar.get(card_id: &str) -> Option<&CardSchedule>
sidecar.orphaned(known_ids: &[String]) -> Vec<&str>
sidecar.missing(known_ids: &[String]) -> Vec<&str>
card_id is always a stringified position index ("0", "1", ...).
Key types:
// Tagged-union JSON: #[serde(tag = "type", rename_all = "snake_case")]
pub enum CardSchedule {
FrontBack {
forward_schedule: Option<Schedule>,
reverse_schedule: Option<Schedule>,
},
Cloze {
blanks: HashMap<String, Schedule>, // keys: "b0", "b1", ...
rects: Vec<RectEntry>, // image region overlays
},
}
pub struct Schedule { pub due: String, pub stability: f32, pub difficulty: f32 }
// due is "YYYY-MM-DD"
pub struct RectEntry {
pub id: String, // "r0", "r1", ...
pub src: String, // bare image stem (no path, no extension)
pub rect: [f32; 4], // normalized [x, y, w, h] in [0.0, 1.0]
pub schedule: Schedule,
}
FSRS helpers: is_due(sched: &Schedule) -> bool, next_schedule(current, days_elapsed, grade) -> Schedule, today_str() -> String.
tala-typst#
Renders typst source strings to RGBA pixels via the typst crate. Implements typst::World. Font loading must provide at least one valid font family or typst panics.
pub fn render(deck_dir: &Path, fragment: &str, preamble: Preamble, blank_spans: &[Range<usize>]) -> Result<RenderResult, Error>
pub enum Preamble {
Authoring, // full card, all content visible
ReviewFront, // FrontBack: show front content block only
ReviewBack, // FrontBack: show back content block only (for bidirectional reverse)
ReviewCloze, // Cloze: replace all blank[] with opaque boxes
}
pub struct RenderResult {
pub rgba: Vec<u8>, // premultiplied alpha, row-major
pub width: u32, pub height: u32,
pub blank_boxes: Vec<[f32; 4]>, // pixel [x,y,w,h] per blank (requires blank_spans)
pub glyph_map: Vec<([f32; 4], Range<usize>, bool)>, // (px rect, frag range, is_math)
pub math_spans: Vec<Range<usize>>, // fragment-relative Equation byte ranges
pub image_boxes: Vec<[f32; 4]>, // pixel [x,y,w,h] per image, document order
}
The RGBA output is premultiplied alpha. Convert to straight alpha before encoding to PNG (divide RGB by alpha).
tala-cli#
Two subcommands:
check <path>— parses all.typfiles, reports card counts, sidecar orphans/missing entries.review <path> [--tag TAG]— interactive terminal review session. Collects all due items across files, prompts grade (1-4), saves to sidecar, git-commits each modified sidecar with message"review session YYYY-MM-DD".
The CLI uses git2 for commits; the GUI does not.
tala (GUI)#
Dioxus desktop app. Single binary. Routes: Home, Editor, Images, Review, Settings.
Global state:
static CARD_DIR: GlobalSignal<PathBuf>
fn card_dir() -> PathBuf
fn cards_path() -> PathBuf // card_dir().join("cards.typ")
fn set_card_dir(path: PathBuf) // persists to ~/.config/tala/dir
Deck dir is loaded from: CLI arg → ~/.config/tala/dir → cwd.
Editor (Editor component):
- Segments source file by blank lines:
SegKind::CardorSegKind::Text, identified by prefix (#card/#cloze). - Each card segment rendered independently. Active card has an editable textarea; inactive cards show rendered preview.
- Auto-saves to disk after 1s debounce. No git commits from the GUI.
- Card identification:
active_card_key()countsCardsegments up to active index → stringified count → sidecar key. - Draw mode: drag to create rect overlays on images within a card. Saved to sidecar as
RectEntry. Existing rects are draggable/resizable. Delete via button. - Image paste from clipboard (arboard, not web_sys) → saves to
<deck>/images/. - Keyboard nav: Up/Down at textarea boundary switches active segment.
Images (Images component):
- Grid of all files in
<deck>/images/. - Rename: updates filename on disk and rewrites matching
srcfields in all sidecarRectEntryrecords. - Copy-name button, lightbox preview.
Review (Review component):
- Setup screen: clickable tag chips (OR/AND toggle for 2+ tags), "Start review →" builds filtered queue.
- Queue items:
ReviewKind::FrontBack,ReviewKind::Cloze,ReviewKind::ImageRect { src, rect }. - Question → reveal → grade (1=Again 2=Hard 3=Good 4=Easy). Keyboard: Space/Enter=reveal, 1-4=grade, S=skip.
- Skip: moves item to back of queue with
skipped: true; terminates when all remaining are skipped. - Image rect review: renders full card via
Authoring, then pixel-fills the rect region for the question image. - Crossfade between question/answer images via CSS grid overlap +
opacitytransition. Click image to toggle. - Saves sidecar to disk after each grade. No git commit.
Settings (Settings component):
- Change deck directory (file picker).
- "Reset all schedules" — clears all schedule data from sidecar (preserves rect geometry), requires confirm dialog.
Deck directory layout#
<deck>/
cards.typ # human-authored typst source, git-tracked
cards.srs.json # machine-managed schedule + rect overlay data
images/ # referenced via #img("stem") or #image("images/stem.ext")
Card edits are byte-span splices — never reformat the whole file.
extract_img_names(source) normalizes both #img("stem") and #image("images/stem.ext") to bare stems, matching sidecar RectEntry.src.