this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 .typ files, 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::Card or SegKind::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() counts Card segments 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 src fields in all sidecar RectEntry records.
  • 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 + opacity transition. 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.