this repo has no description
1
fork

Configure Feed

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

Add README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+205
+205
README.md
··· 1 + # tala 2 + 3 + Flashcard authoring tool. Write cards in Typst, draw cloze regions on rendered previews. 4 + 5 + ## Usage 6 + 7 + ```bash 8 + # Run against a deck directory 9 + cargo run -p tala -- /path/to/deck 10 + 11 + # Or launch without arguments — picks up the last-used directory, 12 + # or falls back to the current working directory. 13 + cargo run -p tala 14 + ``` 15 + 16 + The deck directory must contain (or will create) a `cards.typ` file and an `images/` subdirectory. 17 + 18 + On first launch, use the Home page to pick a folder via the file dialog. 19 + 20 + ### Keyboard shortcuts 21 + 22 + | Key | Action | 23 + |---|---| 24 + | `Ctrl +` / `Ctrl -` | Zoom in / out | 25 + | `Ctrl 0` | Reset zoom | 26 + | `ArrowUp` at first line of textarea | Move focus to previous segment | 27 + | `ArrowDown` at last line of textarea | Move focus to next segment | 28 + 29 + ### Card syntax (Typst) 30 + 31 + ```typst 32 + #card(id: "abc123")[Front content][Back content] 33 + 34 + #cloze(id: "def456")[The answer is #blank[hidden].] 35 + 36 + #img_cloze(id: "ghi789", src: "diagram") 37 + ``` 38 + 39 + 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). 40 + 41 + ### Draw Cloze 42 + 43 + 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[$...$]`. 44 + 45 + ### CLI 46 + 47 + ```bash 48 + # Parse cards and cross-check sidecar schedule data 49 + cargo run -p tala-cli -- check /path/to/deck 50 + 51 + # Review due cards (terminal UI) 52 + cargo run -p tala-cli -- review /path/to/deck 53 + ``` 54 + 55 + --- 56 + 57 + ## Background 58 + 59 + ### Architecture 60 + 61 + Cargo workspace. Crates under `crates/`: 62 + 63 + ``` 64 + tala-format → tala-srs → tala-typst → tala 65 + 66 + tala-cli 67 + ``` 68 + 69 + - **tala-format**: parses `.typ` files via `typst-syntax`. Entry point: `parse_cards(source) -> Vec<CardEntry>`. 70 + - **tala-srs**: loads/saves `cards.srs.json` alongside `cards.typ`. All FSRS schedule data lives here. 71 + - **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. 72 + - **tala**: Dioxus desktop app (this crate). 73 + - **tala-cli**: `check` and `review` subcommands. 74 + 75 + Dioxus routes: `Home` (folder picker) → `Editor` → `Settings` (stub), all inside `Shell` (navbar). Global state: `CARD_DIR: GlobalSignal<PathBuf>` persisted to `~/.config/tala/dir`. 76 + 77 + ### Editor state 78 + 79 + | Signal | Type | Purpose | 80 + |---|---|---| 81 + | `source` | `Signal<String>` | Full `cards.typ` content | 82 + | `save_status` | `Signal<SaveStatus>` | Clean / Dirty / Saved / Error | 83 + | `active_idx` | `Signal<usize>` | Index of focused segment | 84 + | `previews` | `Signal<Vec<Option<Result<PreviewData,_>>>>` | Per-segment typst renders; `None` for text segments | 85 + | `blank_rects_sig` | `Signal<Vec<[f64;4]>>` | Normalized blank positions for the active card | 86 + | `glyph_map_sig` | `Signal<Vec<([f64;4], Range<usize>, bool)>>` | Glyph-to-byte-range map for draw mode | 87 + | `math_spans_sig` | `Signal<Vec<Range<usize>>>` | Equation spans for the active card | 88 + | `draw_mode` | `Signal<bool>` | Draw Cloze mode toggle | 89 + | `drag_start/current` | `Signal<Option<(f64,f64)>>` | Normalized drag coords | 90 + | `drawn_boxes` | `Signal<Vec<[f64;4]>>` | Committed draw rects (shown on failed insertions) | 91 + | `insert_error` | `Signal<Option<String>>` | Draw insertion error message | 92 + 93 + Two resources fire on `source` change: 94 + - `_saver`: 1s debounce → `fs::write` 95 + - `_render`: 300ms debounce → `spawn_blocking` → per-card Typst compile → `previews.set(...)` 96 + 97 + One effect fires on `active_idx` or `previews` change (peeks `source`): copies the active card's render data into the three draw-mode signals. 98 + 99 + ### Algorithms 100 + 101 + **Segmentation** (`make_segments`) 102 + 103 + ``` 104 + split source on runs of \n\n+ 105 + for each non-empty chunk: 106 + if chunk.trim starts with #card[ | #card( | #cloze[ | #cloze( | #img_cloze(: 107 + Seg::Card { start, end } // byte offsets into full source 108 + else: 109 + Seg::Text { start, end } 110 + ``` 111 + 112 + 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. 113 + 114 + **Input splice** (`on_input`) 115 + 116 + ``` 117 + segs = make_segments(source) 118 + seg = segs[active_idx] 119 + new_src = source[..seg.start] + textarea_value + source[seg.end..] 120 + source = new_src 121 + if make_segments(new_src).len() > segs.len(): // user typed \n\n 122 + advance active_idx to the last new segment 123 + JS: focus textarea, move cursor to end 124 + ``` 125 + 126 + **Delete segment** 127 + 128 + ``` 129 + // consume trailing \n\n if present, else leading \n\n 130 + if source[seg.end..].starts_with("\n\n"): remove [seg.start, seg.end+2) 131 + elif source[seg.start-2..seg.start] == "\n\n": remove [seg.start-2, seg.end) 132 + else: remove [seg.start, seg.end) 133 + clamp active_idx to new length 134 + ``` 135 + 136 + **Keyboard boundary navigation** 137 + 138 + ``` 139 + on ArrowUp | ArrowDown: 140 + JS: is cursor on the first (or last) line of the textarea? 141 + if yes: 142 + active_idx ± 1 143 + JS: focus new active textarea 144 + ``` 145 + 146 + **Draw Cloze insertion** 147 + 148 + ``` 149 + on mouseup: 150 + normalize drag rect to [0,1] coords (correcting for CSS zoom) 151 + if rect area < 0.001: discard 152 + 153 + hits = glyphs from glyph_map_sig overlapping rect 154 + if hits empty: show error, keep rect as visual feedback; return 155 + 156 + seg = segs[active_idx] 157 + fragment = source[seg.start..seg.end] 158 + 159 + if all hits are math: 160 + find containing equation span 161 + expand selection to token / bracket boundaries 162 + new_frag = insert_blank_wrap_math(fragment, sel, eq_bounds) 163 + elif any hits are math: 164 + error: "spans math and text — draw over one type only" 165 + else: 166 + new_frag = insert_blank_wrap(fragment, min_byte, max_byte) 167 + 168 + source = source[..seg.start] + new_frag + source[seg.end..] 169 + JS: sync textarea value 170 + ``` 171 + 172 + **Math selection expansion** (`expand_math_selection`) 173 + 174 + Given raw glyph-hit byte range within an equation's inner content: 175 + 1. If `sel_end` is mid-identifier, extend to end of token. 176 + 2. If the token is immediately followed by `(`, include the matched parenthesised group. 177 + 3. Re-balance any unclosed `()` `[]` `{}` pairs by extending `end` rightward. 178 + 179 + ### RSX layout 180 + 181 + ``` 182 + #editor (flex column) 183 + .preview-toolbar 184 + [Draw Cloze toggle] [save status] [insert error] 185 + .card-list (flex column, overflow-y: auto) 186 + for each seg: 187 + .card-row [.active] onclick → activate + focus textarea 188 + .card-actions 189 + .btn-delete × onclick → delete segment 190 + if active: 191 + textarea.card-source onmounted seeds value via JS 192 + match seg: 193 + Seg::Text → pre.text-seg-preview (inactive only) 194 + Seg::Card → 195 + if render error → pre.render-error 196 + elif no image yet → "Rendering…" 197 + else: 198 + .card-preview-wrap 199 + img.card-preview 200 + svg.cloze-overlay yellow blank rects; draw rects if active 201 + if active && draw_mode && cloze: 202 + .draw-capture mouse event handlers 203 + ``` 204 + 205 + > 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.