this repo has no description
1
fork

Configure Feed

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

Rust 95.3%
CSS 4.7%
65 1 4

Clone this repository

https://tangled.org/oscillatory.net/tala https://tangled.org/did:plc:ncwcsl2uejt5ci5qgn7spgis/tala
git@knot.oscillatory.net:oscillatory.net/tala git@knot.oscillatory.net:did:plc:ncwcsl2uejt5ci5qgn7spgis/tala

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

tala#

Flashcard authoring tool. Write cards in Typst, draw cloze regions on rendered previews.

Usage#

# Run against a deck directory
cargo run -p tala -- /path/to/deck

# Or launch without arguments — picks up the last-used directory,
# or falls back to the current working directory.
cargo run -p tala

The deck directory must contain (or will create) a cards.typ file and an images/ subdirectory.

On first launch, use the Home page to pick a folder via the file dialog.

Keyboard shortcuts#

Key Action
Ctrl + / Ctrl - Zoom in / out
Ctrl 0 Reset zoom
ArrowUp at first line of textarea Move focus to previous segment
ArrowDown at last line of textarea Move focus to next segment

Card syntax (Typst)#

#card(id: "abc123")[Front content][Back content]

#cloze(id: "def456")[The answer is #blank[hidden].]

#img_cloze(id: "ghi789", src: "diagram")

Cards are separated by blank lines. Any non-card text between blank lines is treated as a text segment (visible in the editor, ignored for review).

Draw Cloze#

With a #cloze card active, click Draw Cloze in the toolbar, then drag a rectangle over rendered text to wrap it in #blank[...]. The selection snaps to token boundaries and balances brackets automatically. For math, the nearest equation is split and the selection wrapped in #blank[$...$].

CLI#

# Parse cards and cross-check sidecar schedule data
cargo run -p tala-cli -- check /path/to/deck

# Review due cards (terminal UI)
cargo run -p tala-cli -- review /path/to/deck

Background#

Architecture#

Cargo workspace. Crates under crates/:

tala-format  →  tala-srs  →  tala-typst  →  tala
                    ↑
               tala-cli
  • tala-format: parses .typ files via typst-syntax. Entry point: parse_cards(source) -> Vec<CardEntry>.
  • tala-srs: loads/saves cards.srs.json alongside cards.typ. All FSRS schedule data lives here.
  • tala-typst: renders a Typst source string to RGBA via the typst crate. Returns pixel data, blank bounding boxes, a glyph map, and math spans.
  • tala: Dioxus desktop app (this crate).
  • tala-cli: check and review subcommands.

Dioxus routes: Home (folder picker) → EditorSettings (stub), all inside Shell (navbar). Global state: CARD_DIR: GlobalSignal<PathBuf> persisted to ~/.config/tala/dir.

Editor state#

Signal Type Purpose
source Signal<String> Full cards.typ content
save_status Signal<SaveStatus> Clean / Dirty / Saved / Error
active_idx Signal<usize> Index of focused segment
previews Signal<Vec<Option<Result<PreviewData,_>>>> Per-segment typst renders; None for text segments
blank_rects_sig Signal<Vec<[f64;4]>> Normalized blank positions for the active card
glyph_map_sig Signal<Vec<([f64;4], Range<usize>, bool)>> Glyph-to-byte-range map for draw mode
math_spans_sig Signal<Vec<Range<usize>>> Equation spans for the active card
draw_mode Signal<bool> Draw Cloze mode toggle
drag_start/current Signal<Option<(f64,f64)>> Normalized drag coords
drawn_boxes Signal<Vec<[f64;4]>> Committed draw rects (shown on failed insertions)
insert_error Signal<Option<String>> Draw insertion error message

Two resources fire on source change:

  • _saver: 1s debounce → fs::write
  • _render: 300ms debounce → spawn_blocking → per-card Typst compile → previews.set(...)

One effect fires on active_idx or previews change (peeks source): copies the active card's render data into the three draw-mode signals.

Algorithms#

Segmentation (make_segments)

split source on runs of \n\n+
for each non-empty chunk:
  if chunk.trim starts with #card[ | #card( | #cloze[ | #cloze( | #img_cloze(:
    Seg::Card { start, end }   // byte offsets into full source
  else:
    Seg::Text { start, end }

Pure byte scan — Typst is never invoked. Each card segment is compiled independently by _render, so a broken card (unclosed bracket etc.) cannot corrupt adjacent cards.

Input splice (on_input)

segs = make_segments(source)
seg  = segs[active_idx]
new_src = source[..seg.start] + textarea_value + source[seg.end..]
source  = new_src
if make_segments(new_src).len() > segs.len():   // user typed \n\n
  advance active_idx to the last new segment
  JS: focus textarea, move cursor to end

Delete segment

// consume trailing \n\n if present, else leading \n\n
if source[seg.end..].starts_with("\n\n"): remove [seg.start, seg.end+2)
elif source[seg.start-2..seg.start] == "\n\n": remove [seg.start-2, seg.end)
else: remove [seg.start, seg.end)
clamp active_idx to new length

Keyboard boundary navigation

on ArrowUp | ArrowDown:
  JS: is cursor on the first (or last) line of the textarea?
  if yes:
    active_idx ± 1
    JS: focus new active textarea

Draw Cloze insertion

on mouseup:
  normalize drag rect to [0,1] coords (correcting for CSS zoom)
  if rect area < 0.001: discard

  hits = glyphs from glyph_map_sig overlapping rect
  if hits empty: show error, keep rect as visual feedback; return

  seg = segs[active_idx]
  fragment = source[seg.start..seg.end]

  if all hits are math:
    find containing equation span
    expand selection to token / bracket boundaries
    new_frag = insert_blank_wrap_math(fragment, sel, eq_bounds)
  elif any hits are math:
    error: "spans math and text — draw over one type only"
  else:
    new_frag = insert_blank_wrap(fragment, min_byte, max_byte)

  source = source[..seg.start] + new_frag + source[seg.end..]
  JS: sync textarea value

Math selection expansion (expand_math_selection)

Given raw glyph-hit byte range within an equation's inner content:

  1. If sel_end is mid-identifier, extend to end of token.
  2. If the token is immediately followed by (, include the matched parenthesised group.
  3. Re-balance any unclosed () [] {} pairs by extending end rightward.

RSX layout#

#editor  (flex column)
  .preview-toolbar
    [Draw Cloze toggle]  [save status]  [insert error]
  .card-list  (flex column, overflow-y: auto)
    for each seg:
      .card-row [.active]            onclick → activate + focus textarea
        .card-actions
          .btn-delete ×              onclick → delete segment
        if active:
          textarea.card-source       onmounted seeds value via JS
        match seg:
          Seg::Text → pre.text-seg-preview  (inactive only)
          Seg::Card →
            if render error  → pre.render-error
            elif no image yet → "Rendering…"
            else:
              .card-preview-wrap
                img.card-preview
                svg.cloze-overlay    yellow blank rects; draw rects if active
                if active && draw_mode && cloze:
                  .draw-capture      mouse event handlers

Note: textarea value is seeded via a JS onmounted callback (base64-encoding the fragment) rather than Dioxus's value prop, because the webview's controlled-input behavior does not update the DOM value correctly after mount.