this repo has no description
1
fork

Configure Feed

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

feat: filesystem integration -- real card dir, folder picker, autosave

- Replace temp_dir() with OnceLock-backed CARD_DIR; CLI arg or cwd
- OnceLock later promoted to GlobalSignal for runtime mutability
- Home page: "Choose folder" button via rfd native GTK dialog
- Editor: load cards.typ on mount, autosave (1s debounce), save status
- img/img_cloze preambles drop hardcoded .png; World::file() probes
extensions when path has none (png, jpg, jpeg, svg, gif, webp)
- nav-dir, save-status, dir-path CSS added

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

+607 -27
+472 -4
Cargo.lock
··· 149 149 ] 150 150 151 151 [[package]] 152 + name = "ashpd" 153 + version = "0.11.1" 154 + source = "registry+https://github.com/rust-lang/crates.io-index" 155 + checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" 156 + dependencies = [ 157 + "async-fs", 158 + "async-net", 159 + "enumflags2", 160 + "futures-channel", 161 + "futures-util", 162 + "rand 0.9.2", 163 + "raw-window-handle 0.6.2", 164 + "serde", 165 + "serde_repr", 166 + "url", 167 + "wayland-backend", 168 + "wayland-client", 169 + "wayland-protocols", 170 + "zbus", 171 + ] 172 + 173 + [[package]] 174 + name = "async-broadcast" 175 + version = "0.7.2" 176 + source = "registry+https://github.com/rust-lang/crates.io-index" 177 + checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" 178 + dependencies = [ 179 + "event-listener", 180 + "event-listener-strategy", 181 + "futures-core", 182 + "pin-project-lite", 183 + ] 184 + 185 + [[package]] 152 186 name = "async-channel" 153 187 version = "2.5.0" 154 188 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 161 195 ] 162 196 163 197 [[package]] 198 + name = "async-executor" 199 + version = "1.14.0" 200 + source = "registry+https://github.com/rust-lang/crates.io-index" 201 + checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" 202 + dependencies = [ 203 + "async-task", 204 + "concurrent-queue", 205 + "fastrand", 206 + "futures-lite", 207 + "pin-project-lite", 208 + "slab", 209 + ] 210 + 211 + [[package]] 212 + name = "async-fs" 213 + version = "2.2.0" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" 216 + dependencies = [ 217 + "async-lock", 218 + "blocking", 219 + "futures-lite", 220 + ] 221 + 222 + [[package]] 223 + name = "async-io" 224 + version = "2.6.0" 225 + source = "registry+https://github.com/rust-lang/crates.io-index" 226 + checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" 227 + dependencies = [ 228 + "autocfg", 229 + "cfg-if", 230 + "concurrent-queue", 231 + "futures-io", 232 + "futures-lite", 233 + "parking", 234 + "polling", 235 + "rustix", 236 + "slab", 237 + "windows-sys 0.61.2", 238 + ] 239 + 240 + [[package]] 241 + name = "async-lock" 242 + version = "3.4.2" 243 + source = "registry+https://github.com/rust-lang/crates.io-index" 244 + checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" 245 + dependencies = [ 246 + "event-listener", 247 + "event-listener-strategy", 248 + "pin-project-lite", 249 + ] 250 + 251 + [[package]] 252 + name = "async-net" 253 + version = "2.0.0" 254 + source = "registry+https://github.com/rust-lang/crates.io-index" 255 + checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" 256 + dependencies = [ 257 + "async-io", 258 + "blocking", 259 + "futures-lite", 260 + ] 261 + 262 + [[package]] 263 + name = "async-process" 264 + version = "2.5.0" 265 + source = "registry+https://github.com/rust-lang/crates.io-index" 266 + checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" 267 + dependencies = [ 268 + "async-channel", 269 + "async-io", 270 + "async-lock", 271 + "async-signal", 272 + "async-task", 273 + "blocking", 274 + "cfg-if", 275 + "event-listener", 276 + "futures-lite", 277 + "rustix", 278 + ] 279 + 280 + [[package]] 281 + name = "async-recursion" 282 + version = "1.1.1" 283 + source = "registry+https://github.com/rust-lang/crates.io-index" 284 + checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" 285 + dependencies = [ 286 + "proc-macro2", 287 + "quote", 288 + "syn 2.0.117", 289 + ] 290 + 291 + [[package]] 292 + name = "async-signal" 293 + version = "0.2.13" 294 + source = "registry+https://github.com/rust-lang/crates.io-index" 295 + checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" 296 + dependencies = [ 297 + "async-io", 298 + "async-lock", 299 + "atomic-waker", 300 + "cfg-if", 301 + "futures-core", 302 + "futures-io", 303 + "rustix", 304 + "signal-hook-registry", 305 + "slab", 306 + "windows-sys 0.61.2", 307 + ] 308 + 309 + [[package]] 164 310 name = "async-stream" 165 311 version = "0.3.6" 166 312 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 183 329 ] 184 330 185 331 [[package]] 332 + name = "async-task" 333 + version = "4.7.1" 334 + source = "registry+https://github.com/rust-lang/crates.io-index" 335 + checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 336 + 337 + [[package]] 186 338 name = "async-trait" 187 339 version = "0.1.89" 188 340 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 404 556 ] 405 557 406 558 [[package]] 559 + name = "blocking" 560 + version = "1.6.2" 561 + source = "registry+https://github.com/rust-lang/crates.io-index" 562 + checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" 563 + dependencies = [ 564 + "async-channel", 565 + "async-task", 566 + "futures-io", 567 + "futures-lite", 568 + "piper", 569 + ] 570 + 571 + [[package]] 407 572 name = "bumpalo" 408 573 version = "3.20.2" 409 574 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 894 1059 source = "registry+https://github.com/rust-lang/crates.io-index" 895 1060 checksum = "1f6597e8bdbca37f1f56e5a80d15857b0932aead21a78d20de49e99e74933046" 896 1061 dependencies = [ 897 - "quick-xml", 1062 + "quick-xml 0.38.4", 898 1063 "serde", 899 1064 ] 900 1065 ··· 1997 2162 "objc_id", 1998 2163 "percent-encoding", 1999 2164 "rand 0.9.2", 2000 - "rfd", 2165 + "rfd 0.17.2", 2001 2166 "rustc-hash 2.1.1", 2002 2167 "serde", 2003 2168 "serde_json", ··· 2445 2610 ] 2446 2611 2447 2612 [[package]] 2613 + name = "dlib" 2614 + version = "0.5.3" 2615 + source = "registry+https://github.com/rust-lang/crates.io-index" 2616 + checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" 2617 + dependencies = [ 2618 + "libloading 0.8.9", 2619 + ] 2620 + 2621 + [[package]] 2448 2622 name = "dlopen2" 2449 2623 version = "0.8.2" 2450 2624 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2477 2651 ] 2478 2652 2479 2653 [[package]] 2654 + name = "downcast-rs" 2655 + version = "1.2.1" 2656 + source = "registry+https://github.com/rust-lang/crates.io-index" 2657 + checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 2658 + 2659 + [[package]] 2480 2660 name = "dpi" 2481 2661 version = "0.1.2" 2482 2662 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2572 2752 ] 2573 2753 2574 2754 [[package]] 2755 + name = "endi" 2756 + version = "1.1.1" 2757 + source = "registry+https://github.com/rust-lang/crates.io-index" 2758 + checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" 2759 + 2760 + [[package]] 2575 2761 name = "enum-as-inner" 2576 2762 version = "0.6.1" 2577 2763 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2604 2790 ] 2605 2791 2606 2792 [[package]] 2793 + name = "enumflags2" 2794 + version = "0.7.12" 2795 + source = "registry+https://github.com/rust-lang/crates.io-index" 2796 + checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" 2797 + dependencies = [ 2798 + "enumflags2_derive", 2799 + "serde", 2800 + ] 2801 + 2802 + [[package]] 2803 + name = "enumflags2_derive" 2804 + version = "0.7.12" 2805 + source = "registry+https://github.com/rust-lang/crates.io-index" 2806 + checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" 2807 + dependencies = [ 2808 + "proc-macro2", 2809 + "quote", 2810 + "syn 2.0.117", 2811 + ] 2812 + 2813 + [[package]] 2607 2814 name = "enumset" 2608 2815 version = "1.1.10" 2609 2816 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3893 4100 version = "0.5.2" 3894 4101 source = "registry+https://github.com/rust-lang/crates.io-index" 3895 4102 checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 4103 + 4104 + [[package]] 4105 + name = "hex" 4106 + version = "0.4.3" 4107 + source = "registry+https://github.com/rust-lang/crates.io-index" 4108 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 3896 4109 3897 4110 [[package]] 3898 4111 name = "hexf-parse" ··· 5580 5793 ] 5581 5794 5582 5795 [[package]] 5796 + name = "ordered-stream" 5797 + version = "0.2.0" 5798 + source = "registry+https://github.com/rust-lang/crates.io-index" 5799 + checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" 5800 + dependencies = [ 5801 + "futures-core", 5802 + "pin-project-lite", 5803 + ] 5804 + 5805 + [[package]] 5583 5806 name = "palette" 5584 5807 version = "0.7.6" 5585 5808 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5869 6092 version = "0.1.0" 5870 6093 source = "registry+https://github.com/rust-lang/crates.io-index" 5871 6094 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 6095 + 6096 + [[package]] 6097 + name = "piper" 6098 + version = "0.2.5" 6099 + source = "registry+https://github.com/rust-lang/crates.io-index" 6100 + checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" 6101 + dependencies = [ 6102 + "atomic-waker", 6103 + "fastrand", 6104 + "futures-io", 6105 + ] 5872 6106 5873 6107 [[package]] 5874 6108 name = "pixglyph" ··· 5893 6127 dependencies = [ 5894 6128 "base64", 5895 6129 "indexmap", 5896 - "quick-xml", 6130 + "quick-xml 0.38.4", 5897 6131 "serde", 5898 6132 "time", 5899 6133 ] ··· 5925 6159 ] 5926 6160 5927 6161 [[package]] 6162 + name = "polling" 6163 + version = "3.11.0" 6164 + source = "registry+https://github.com/rust-lang/crates.io-index" 6165 + checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" 6166 + dependencies = [ 6167 + "cfg-if", 6168 + "concurrent-queue", 6169 + "hermit-abi", 6170 + "pin-project-lite", 6171 + "rustix", 6172 + "windows-sys 0.61.2", 6173 + ] 6174 + 6175 + [[package]] 5928 6176 name = "pollster" 5929 6177 version = "0.4.0" 5930 6178 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6184 6432 ] 6185 6433 6186 6434 [[package]] 6435 + name = "quick-xml" 6436 + version = "0.39.2" 6437 + source = "registry+https://github.com/rust-lang/crates.io-index" 6438 + checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" 6439 + dependencies = [ 6440 + "memchr", 6441 + ] 6442 + 6443 + [[package]] 6187 6444 name = "quinn" 6188 6445 version = "0.11.9" 6189 6446 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6235 6492 "once_cell", 6236 6493 "socket2", 6237 6494 "tracing", 6238 - "windows-sys 0.59.0", 6495 + "windows-sys 0.60.2", 6239 6496 ] 6240 6497 6241 6498 [[package]] ··· 6592 6849 6593 6850 [[package]] 6594 6851 name = "rfd" 6852 + version = "0.15.4" 6853 + source = "registry+https://github.com/rust-lang/crates.io-index" 6854 + checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" 6855 + dependencies = [ 6856 + "ashpd", 6857 + "block2", 6858 + "dispatch2", 6859 + "js-sys", 6860 + "log", 6861 + "objc2", 6862 + "objc2-app-kit", 6863 + "objc2-core-foundation", 6864 + "objc2-foundation", 6865 + "pollster", 6866 + "raw-window-handle 0.6.2", 6867 + "urlencoding", 6868 + "wasm-bindgen", 6869 + "wasm-bindgen-futures", 6870 + "web-sys", 6871 + "windows-sys 0.59.0", 6872 + ] 6873 + 6874 + [[package]] 6875 + name = "rfd" 6595 6876 version = "0.17.2" 6596 6877 source = "registry+https://github.com/rust-lang/crates.io-index" 6597 6878 checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" ··· 6855 7136 ] 6856 7137 6857 7138 [[package]] 7139 + name = "scoped-tls" 7140 + version = "1.0.1" 7141 + source = "registry+https://github.com/rust-lang/crates.io-index" 7142 + checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 7143 + 7144 + [[package]] 6858 7145 name = "scopeguard" 6859 7146 version = "1.2.0" 6860 7147 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7601 7888 "base64", 7602 7889 "dioxus", 7603 7890 "image", 7891 + "rfd 0.15.4", 7604 7892 "tala-format", 7605 7893 "tala-typst", 7606 7894 "tokio", ··· 8524 8812 ] 8525 8813 8526 8814 [[package]] 8815 + name = "uds_windows" 8816 + version = "1.2.1" 8817 + source = "registry+https://github.com/rust-lang/crates.io-index" 8818 + checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" 8819 + dependencies = [ 8820 + "memoffset", 8821 + "tempfile", 8822 + "windows-sys 0.61.2", 8823 + ] 8824 + 8825 + [[package]] 8527 8826 name = "ug" 8528 8827 version = "0.1.0" 8529 8828 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8705 9004 "serde", 8706 9005 "serde_derive", 8707 9006 ] 9007 + 9008 + [[package]] 9009 + name = "urlencoding" 9010 + version = "2.1.3" 9011 + source = "registry+https://github.com/rust-lang/crates.io-index" 9012 + checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 8708 9013 8709 9014 [[package]] 8710 9015 name = "usvg" ··· 8758 9063 checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" 8759 9064 dependencies = [ 8760 9065 "js-sys", 9066 + "serde_core", 8761 9067 "wasm-bindgen", 8762 9068 ] 8763 9069 ··· 9020 9326 ] 9021 9327 9022 9328 [[package]] 9329 + name = "wayland-backend" 9330 + version = "0.3.14" 9331 + source = "registry+https://github.com/rust-lang/crates.io-index" 9332 + checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" 9333 + dependencies = [ 9334 + "cc", 9335 + "downcast-rs", 9336 + "rustix", 9337 + "scoped-tls", 9338 + "smallvec", 9339 + "wayland-sys", 9340 + ] 9341 + 9342 + [[package]] 9343 + name = "wayland-client" 9344 + version = "0.31.13" 9345 + source = "registry+https://github.com/rust-lang/crates.io-index" 9346 + checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" 9347 + dependencies = [ 9348 + "bitflags 2.11.0", 9349 + "rustix", 9350 + "wayland-backend", 9351 + "wayland-scanner", 9352 + ] 9353 + 9354 + [[package]] 9355 + name = "wayland-protocols" 9356 + version = "0.32.11" 9357 + source = "registry+https://github.com/rust-lang/crates.io-index" 9358 + checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" 9359 + dependencies = [ 9360 + "bitflags 2.11.0", 9361 + "wayland-backend", 9362 + "wayland-client", 9363 + "wayland-scanner", 9364 + ] 9365 + 9366 + [[package]] 9367 + name = "wayland-scanner" 9368 + version = "0.31.9" 9369 + source = "registry+https://github.com/rust-lang/crates.io-index" 9370 + checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" 9371 + dependencies = [ 9372 + "proc-macro2", 9373 + "quick-xml 0.39.2", 9374 + "quote", 9375 + ] 9376 + 9377 + [[package]] 9378 + name = "wayland-sys" 9379 + version = "0.31.10" 9380 + source = "registry+https://github.com/rust-lang/crates.io-index" 9381 + checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" 9382 + dependencies = [ 9383 + "dlib", 9384 + "log", 9385 + "pkg-config", 9386 + ] 9387 + 9388 + [[package]] 9023 9389 name = "web-sys" 9024 9390 version = "0.3.91" 9025 9391 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 10175 10541 ] 10176 10542 10177 10543 [[package]] 10544 + name = "zbus" 10545 + version = "5.14.0" 10546 + source = "registry+https://github.com/rust-lang/crates.io-index" 10547 + checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" 10548 + dependencies = [ 10549 + "async-broadcast", 10550 + "async-executor", 10551 + "async-io", 10552 + "async-lock", 10553 + "async-process", 10554 + "async-recursion", 10555 + "async-task", 10556 + "async-trait", 10557 + "blocking", 10558 + "enumflags2", 10559 + "event-listener", 10560 + "futures-core", 10561 + "futures-lite", 10562 + "hex", 10563 + "libc", 10564 + "ordered-stream", 10565 + "rustix", 10566 + "serde", 10567 + "serde_repr", 10568 + "tracing", 10569 + "uds_windows", 10570 + "uuid", 10571 + "windows-sys 0.61.2", 10572 + "winnow 0.7.15", 10573 + "zbus_macros", 10574 + "zbus_names", 10575 + "zvariant", 10576 + ] 10577 + 10578 + [[package]] 10579 + name = "zbus_macros" 10580 + version = "5.14.0" 10581 + source = "registry+https://github.com/rust-lang/crates.io-index" 10582 + checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" 10583 + dependencies = [ 10584 + "proc-macro-crate 3.5.0", 10585 + "proc-macro2", 10586 + "quote", 10587 + "syn 2.0.117", 10588 + "zbus_names", 10589 + "zvariant", 10590 + "zvariant_utils", 10591 + ] 10592 + 10593 + [[package]] 10594 + name = "zbus_names" 10595 + version = "4.3.1" 10596 + source = "registry+https://github.com/rust-lang/crates.io-index" 10597 + checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" 10598 + dependencies = [ 10599 + "serde", 10600 + "winnow 0.7.15", 10601 + "zvariant", 10602 + ] 10603 + 10604 + [[package]] 10178 10605 name = "zerocopy" 10179 10606 version = "0.8.47" 10180 10607 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 10340 10767 dependencies = [ 10341 10768 "zune-core 0.5.1", 10342 10769 ] 10770 + 10771 + [[package]] 10772 + name = "zvariant" 10773 + version = "5.10.0" 10774 + source = "registry+https://github.com/rust-lang/crates.io-index" 10775 + checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" 10776 + dependencies = [ 10777 + "endi", 10778 + "enumflags2", 10779 + "serde", 10780 + "url", 10781 + "winnow 0.7.15", 10782 + "zvariant_derive", 10783 + "zvariant_utils", 10784 + ] 10785 + 10786 + [[package]] 10787 + name = "zvariant_derive" 10788 + version = "5.10.0" 10789 + source = "registry+https://github.com/rust-lang/crates.io-index" 10790 + checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" 10791 + dependencies = [ 10792 + "proc-macro-crate 3.5.0", 10793 + "proc-macro2", 10794 + "quote", 10795 + "syn 2.0.117", 10796 + "zvariant_utils", 10797 + ] 10798 + 10799 + [[package]] 10800 + name = "zvariant_utils" 10801 + version = "3.3.0" 10802 + source = "registry+https://github.com/rust-lang/crates.io-index" 10803 + checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" 10804 + dependencies = [ 10805 + "proc-macro2", 10806 + "quote", 10807 + "serde", 10808 + "syn 2.0.117", 10809 + "winnow 0.7.15", 10810 + ]
+10 -11
crates/tala-typst/src/lib.rs
··· 316 316 } 317 317 #let blank(..args) = args.pos().at(0, default: []) 318 318 #let cloze(tags: (), ..args) = args.pos().at(0, default: []) 319 - #let img_cloze(src: "") = image("images/" + src + ".png") 320 - #let img(name) = image("images/" + name + ".png") 319 + #let img_cloze(src: "") = image("images/" + src) 320 + #let img(name) = image("images/" + name) 321 321 "#; 322 322 323 323 /// FrontBack: front side only; other card types unchanged. ··· 326 326 #let card(dir: "fwd", tags: (), ..args) = args.pos().at(0, default: []) 327 327 #let blank(..args) = args.pos().at(0, default: []) 328 328 #let cloze(tags: (), ..args) = args.pos().at(0, default: []) 329 - #let img_cloze(src: "") = image("images/" + src + ".png") 330 - #let img(name) = image("images/" + name + ".png") 329 + #let img_cloze(src: "") = image("images/" + src) 330 + #let img(name) = image("images/" + name) 331 331 "#; 332 332 333 333 /// Cloze review: blank contents replaced by fixed-width underline boxes. ··· 343 343 } 344 344 #let blank(..args) = box(width: 4em, stroke: (bottom: 0.5pt), inset: (bottom: 2pt))[] 345 345 #let cloze(tags: (), ..args) = args.pos().at(0, default: []) 346 - #let img_cloze(src: "") = image("images/" + src + ".png") 347 - #let img(name) = image("images/" + name + ".png") 346 + #let img_cloze(src: "") = image("images/" + src) 347 + #let img(name) = image("images/" + name) 348 348 "#; 349 349 350 350 // ── World implementation ────────────────────────────────────────────────────── ··· 408 408 .map_err(|_| FileError::NotFound(path)); 409 409 } 410 410 411 - // For image references that omit extension, try common formats. 412 - if path.extension().map_or(false, |e| e == "png") { 413 - let stem = path.with_extension(""); 414 - for ext in ["jpg", "jpeg", "svg", "gif", "webp"] { 415 - let candidate = stem.with_extension(ext); 411 + // No extension: probe common image formats. 412 + if path.extension().is_none() { 413 + for ext in ["png", "jpg", "jpeg", "svg", "gif", "webp"] { 414 + let candidate = path.with_extension(ext); 416 415 if candidate.exists() { 417 416 return std::fs::read(&candidate) 418 417 .map(Bytes::new)
+1
crates/tala/Cargo.toml
··· 13 13 image = { workspace = true } 14 14 base64 = "0.22" 15 15 tokio = { version = "1", features = ["time"] } 16 + rfd = "0.15" 16 17 17 18 [features] 18 19 default = ["desktop"]
+24
crates/tala/assets/main.css
··· 39 39 background: #1e2130; 40 40 } 41 41 42 + .nav-dir { 43 + margin-left: auto; 44 + color: #555c70; 45 + font-size: 12px; 46 + font-family: 'JetBrains Mono', 'Fira Code', monospace; 47 + } 48 + 42 49 /* ── Page container (fills space below navbar) ───────────────────────────── */ 43 50 #home, 44 51 #settings { ··· 184 191 word-break: break-word; 185 192 max-width: 100%; 186 193 } 194 + 195 + /* ── Save status ────────────────────────────────────────────────────────── */ 196 + .save-status { 197 + font-size: 11px; 198 + padding: 2px 6px; 199 + border-radius: 3px; 200 + } 201 + .save-status.dirty { color: #c9a040; } 202 + .save-status.saved { color: #5a8a5a; } 203 + .save-status.error { color: #e06c75; } 204 + 205 + /* ── Home ───────────────────────────────────────────────────────────────── */ 206 + .dir-path { 207 + color: #555c70; 208 + font-size: 12px; 209 + font-family: 'JetBrains Mono', 'Fira Code', monospace; 210 + }
+100 -12
crates/tala/src/main.rs
··· 9 9 const FAVICON: Asset = asset!("/assets/favicon.ico"); 10 10 const MAIN_CSS: Asset = asset!("/assets/main.css"); 11 11 12 + /// Directory containing cards.typ and images/. 13 + static CARD_DIR: GlobalSignal<PathBuf> = Signal::global(|| { 14 + let dir = std::env::args() 15 + .nth(1) 16 + .map(PathBuf::from) 17 + .unwrap_or_else(|| std::env::current_dir().expect("cwd")); 18 + let dir = std::fs::canonicalize(&dir).unwrap_or(dir); 19 + let _ = std::fs::create_dir_all(dir.join("images")); 20 + dir 21 + }); 22 + 23 + fn card_dir() -> PathBuf { 24 + CARD_DIR.read().clone() 25 + } 26 + 27 + fn cards_path() -> PathBuf { 28 + card_dir().join("cards.typ") 29 + } 30 + 31 + fn set_card_dir(path: PathBuf) { 32 + let path = std::fs::canonicalize(&path).unwrap_or(path); 33 + let _ = std::fs::create_dir_all(path.join("images")); 34 + *CARD_DIR.write() = path; 35 + } 36 + 12 37 // Ctrl+/- zoom and Ctrl+0 reset, persisted in sessionStorage. 13 38 const ZOOM_JS: &str = r#" 14 39 (function() { ··· 85 110 /// Persistent shell: navbar + page content. 86 111 #[component] 87 112 fn Shell() -> Element { 113 + let dir = CARD_DIR.read(); 114 + let dir_name = dir 115 + .file_name() 116 + .map(|n| n.to_string_lossy().into_owned()) 117 + .unwrap_or_else(|| dir.display().to_string()); 88 118 rsx! { 89 119 nav { id: "navbar", 90 120 Link { to: Route::Home {}, "Home" } 91 121 Link { to: Route::Editor {}, "Editor" } 92 122 Link { to: Route::Settings {}, "Settings" } 123 + span { class: "nav-dir", "{dir_name}" } 93 124 } 94 125 Outlet::<Route> {} 95 126 } ··· 97 128 98 129 #[component] 99 130 fn Home() -> Element { 131 + let dir_display = CARD_DIR.read().display().to_string(); 100 132 rsx! { 101 133 div { id: "home", 102 134 h2 { "tala" } 103 - p { "Select a deck to begin." } 104 - Link { to: Route::Editor {}, "Open editor →" } 135 + p { class: "dir-path", "{dir_display}" } 136 + div { style: "display:flex; gap:12px; align-items:center; margin-top:8px;", 137 + button { 138 + class: "btn", 139 + onclick: move |_| { 140 + spawn(async move { 141 + if let Some(handle) = rfd::AsyncFileDialog::new() 142 + .set_title("Select card directory") 143 + .pick_folder() 144 + .await 145 + { 146 + set_card_dir(handle.path().to_path_buf()); 147 + } 148 + }); 149 + }, 150 + "Choose folder" 151 + } 152 + Link { to: Route::Editor {}, "Open editor" } 153 + } 105 154 } 106 155 } 107 156 } ··· 116 165 } 117 166 } 118 167 168 + #[derive(Clone, PartialEq)] 169 + enum SaveStatus { 170 + Clean, 171 + Dirty, 172 + Saved, 173 + Error(String), 174 + } 175 + 119 176 struct PreviewData { 120 177 b64: String, 121 178 img_w: u32, ··· 130 187 131 188 #[component] 132 189 fn Editor() -> Element { 190 + // Load source from cards.typ (empty string if file doesn't exist yet). 133 191 let mut source = use_signal(|| { 134 - r#"#cloze[The Gaussian integral is $integral_(-infinity)^(infinity) e^(-x^2) dif x = sqrt(pi)$]"# 135 - .to_string() 192 + std::fs::read_to_string(cards_path()).unwrap_or_default() 136 193 }); 137 194 138 - let preview = use_resource(move || async move { 195 + // Track save state: None = clean, Some(text) = pending save. 196 + let mut save_status = use_signal(|| SaveStatus::Clean); 197 + 198 + // Auto-save: debounce 1s after last edit, write to disk. 199 + let _saver = use_resource(move || async move { 139 200 let text = source.read().clone(); 140 - tokio::time::sleep(Duration::from_millis(300)).await; 141 - tokio::task::spawn_blocking(move || render_preview(&text)) 142 - .await 143 - .unwrap_or_else(|e| Err(e.to_string())) 201 + tokio::time::sleep(Duration::from_millis(1000)).await; 202 + let path = cards_path(); 203 + match tokio::task::spawn_blocking(move || std::fs::write(&path, &text)).await { 204 + Ok(Ok(())) => save_status.set(SaveStatus::Saved), 205 + Ok(Err(e)) => save_status.set(SaveStatus::Error(e.to_string())), 206 + Err(e) => save_status.set(SaveStatus::Error(e.to_string())), 207 + } 208 + }); 209 + 210 + // Mark dirty on every edit. 211 + let on_input = move |e: FormEvent| { 212 + source.set(e.value()); 213 + save_status.set(SaveStatus::Dirty); 214 + }; 215 + 216 + let dir = card_dir().clone(); 217 + let preview = use_resource(move || { 218 + let dir = dir.clone(); 219 + async move { 220 + let text = source.read().clone(); 221 + tokio::time::sleep(Duration::from_millis(300)).await; 222 + tokio::task::spawn_blocking(move || render_preview(&text, &dir)) 223 + .await 224 + .unwrap_or_else(|e| Err(e.to_string())) 225 + } 144 226 }); 145 227 146 228 // Blank rects, glyph map, and math spans synced from latest preview (needed in event closures). ··· 174 256 id: "card-source", 175 257 spellcheck: "false", 176 258 value: "{source}", 177 - oninput: move |e| source.set(e.value()), 259 + oninput: on_input, 178 260 } 179 261 } 180 262 div { ··· 194 276 insert_error.set(None); 195 277 }, 196 278 if *draw_mode.read() { "Exit Draw" } else { "Draw Cloze" } 279 + } 280 + match &*save_status.read() { 281 + SaveStatus::Clean => rsx! {}, 282 + SaveStatus::Dirty => rsx! { span { class: "save-status dirty", "Unsaved" } }, 283 + SaveStatus::Saved => rsx! { span { class: "save-status saved", "Saved" } }, 284 + SaveStatus::Error(e) => rsx! { span { class: "save-status error", "Save error: {e}" } }, 197 285 } 198 286 if let Some(err) = &*insert_error.read() { 199 287 span { class: "insert-error", "{err}" } ··· 648 736 } 649 737 } 650 738 651 - fn render_preview(source: &str) -> Result<PreviewData, String> { 652 - let deck_dir = PathBuf::from(std::env::temp_dir()); 739 + fn render_preview(source: &str, dir: &PathBuf) -> Result<PreviewData, String> { 740 + let deck_dir = dir.clone(); 653 741 654 742 // Collect content_spans of all blanks in document order. 655 743 let cards = tala_format::parse_cards(source);