magical markdown slides

feat: image rendering

* render alt text as image captions

* fix: nested list rendering

+1552 -25
+998 -5
Cargo.lock
··· 9 9 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 10 11 11 [[package]] 12 + name = "aligned" 13 + version = "0.4.2" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" 16 + dependencies = [ 17 + "as-slice", 18 + ] 19 + 20 + [[package]] 21 + name = "aligned-vec" 22 + version = "0.6.4" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" 25 + dependencies = [ 26 + "equator", 27 + ] 28 + 29 + [[package]] 12 30 name = "allocator-api2" 13 31 version = "0.2.21" 14 32 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 71 89 checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 72 90 73 91 [[package]] 92 + name = "arbitrary" 93 + version = "1.4.2" 94 + source = "registry+https://github.com/rust-lang/crates.io-index" 95 + checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" 96 + 97 + [[package]] 98 + name = "arg_enum_proc_macro" 99 + version = "0.3.4" 100 + source = "registry+https://github.com/rust-lang/crates.io-index" 101 + checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" 102 + dependencies = [ 103 + "proc-macro2", 104 + "quote", 105 + "syn", 106 + ] 107 + 108 + [[package]] 109 + name = "arrayvec" 110 + version = "0.7.6" 111 + source = "registry+https://github.com/rust-lang/crates.io-index" 112 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 113 + 114 + [[package]] 115 + name = "as-slice" 116 + version = "0.2.1" 117 + source = "registry+https://github.com/rust-lang/crates.io-index" 118 + checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" 119 + dependencies = [ 120 + "stable_deref_trait", 121 + ] 122 + 123 + [[package]] 124 + name = "autocfg" 125 + version = "1.5.0" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 128 + 129 + [[package]] 130 + name = "av-scenechange" 131 + version = "0.14.1" 132 + source = "registry+https://github.com/rust-lang/crates.io-index" 133 + checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" 134 + dependencies = [ 135 + "aligned", 136 + "anyhow", 137 + "arg_enum_proc_macro", 138 + "arrayvec", 139 + "log", 140 + "num-rational", 141 + "num-traits", 142 + "pastey", 143 + "rayon", 144 + "thiserror 2.0.17", 145 + "v_frame", 146 + "y4m", 147 + ] 148 + 149 + [[package]] 150 + name = "av1-grain" 151 + version = "0.2.5" 152 + source = "registry+https://github.com/rust-lang/crates.io-index" 153 + checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" 154 + dependencies = [ 155 + "anyhow", 156 + "arrayvec", 157 + "log", 158 + "nom", 159 + "num-rational", 160 + "v_frame", 161 + ] 162 + 163 + [[package]] 164 + name = "avif-serialize" 165 + version = "0.8.6" 166 + source = "registry+https://github.com/rust-lang/crates.io-index" 167 + checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" 168 + dependencies = [ 169 + "arrayvec", 170 + ] 171 + 172 + [[package]] 74 173 name = "base64" 75 174 version = "0.22.1" 76 175 source = "registry+https://github.com/rust-lang/crates.io-index" 77 176 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 78 177 79 178 [[package]] 179 + name = "base64-simd" 180 + version = "0.8.0" 181 + source = "registry+https://github.com/rust-lang/crates.io-index" 182 + checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" 183 + dependencies = [ 184 + "outref", 185 + "vsimd", 186 + ] 187 + 188 + [[package]] 80 189 name = "bincode" 81 190 version = "1.3.3" 82 191 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 86 195 ] 87 196 88 197 [[package]] 198 + name = "bit_field" 199 + version = "0.10.3" 200 + source = "registry+https://github.com/rust-lang/crates.io-index" 201 + checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" 202 + 203 + [[package]] 89 204 name = "bitflags" 90 205 version = "2.9.4" 91 206 source = "registry+https://github.com/rust-lang/crates.io-index" 92 207 checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 93 208 94 209 [[package]] 210 + name = "bitstream-io" 211 + version = "4.9.0" 212 + source = "registry+https://github.com/rust-lang/crates.io-index" 213 + checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" 214 + dependencies = [ 215 + "core2", 216 + ] 217 + 218 + [[package]] 219 + name = "built" 220 + version = "0.8.0" 221 + source = "registry+https://github.com/rust-lang/crates.io-index" 222 + checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" 223 + 224 + [[package]] 225 + name = "bumpalo" 226 + version = "3.19.0" 227 + source = "registry+https://github.com/rust-lang/crates.io-index" 228 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 229 + 230 + [[package]] 231 + name = "bytemuck" 232 + version = "1.24.0" 233 + source = "registry+https://github.com/rust-lang/crates.io-index" 234 + checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" 235 + 236 + [[package]] 237 + name = "byteorder-lite" 238 + version = "0.1.0" 239 + source = "registry+https://github.com/rust-lang/crates.io-index" 240 + checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 241 + 242 + [[package]] 95 243 name = "cassowary" 96 244 version = "0.3.0" 97 245 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 113 261 checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" 114 262 dependencies = [ 115 263 "find-msvc-tools", 264 + "jobserver", 265 + "libc", 116 266 "shlex", 117 267 ] 118 268 ··· 163 313 checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 164 314 165 315 [[package]] 316 + name = "color_quant" 317 + version = "1.1.0" 318 + source = "registry+https://github.com/rust-lang/crates.io-index" 319 + checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 320 + 321 + [[package]] 166 322 name = "colorchoice" 167 323 version = "1.0.4" 168 324 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 192 348 ] 193 349 194 350 [[package]] 351 + name = "core2" 352 + version = "0.4.0" 353 + source = "registry+https://github.com/rust-lang/crates.io-index" 354 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 355 + dependencies = [ 356 + "memchr", 357 + ] 358 + 359 + [[package]] 195 360 name = "crc32fast" 196 361 version = "1.5.0" 197 362 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 201 366 ] 202 367 203 368 [[package]] 369 + name = "crossbeam-deque" 370 + version = "0.8.6" 371 + source = "registry+https://github.com/rust-lang/crates.io-index" 372 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 373 + dependencies = [ 374 + "crossbeam-epoch", 375 + "crossbeam-utils", 376 + ] 377 + 378 + [[package]] 379 + name = "crossbeam-epoch" 380 + version = "0.9.18" 381 + source = "registry+https://github.com/rust-lang/crates.io-index" 382 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 383 + dependencies = [ 384 + "crossbeam-utils", 385 + ] 386 + 387 + [[package]] 388 + name = "crossbeam-utils" 389 + version = "0.8.21" 390 + source = "registry+https://github.com/rust-lang/crates.io-index" 391 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 392 + 393 + [[package]] 204 394 name = "crossterm" 205 395 version = "0.28.1" 206 396 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 242 432 dependencies = [ 243 433 "winapi", 244 434 ] 435 + 436 + [[package]] 437 + name = "crunchy" 438 + version = "0.2.4" 439 + source = "registry+https://github.com/rust-lang/crates.io-index" 440 + checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 245 441 246 442 [[package]] 247 443 name = "darling" ··· 324 520 checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 325 521 326 522 [[package]] 523 + name = "equator" 524 + version = "0.4.2" 525 + source = "registry+https://github.com/rust-lang/crates.io-index" 526 + checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" 527 + dependencies = [ 528 + "equator-macro", 529 + ] 530 + 531 + [[package]] 532 + name = "equator-macro" 533 + version = "0.4.2" 534 + source = "registry+https://github.com/rust-lang/crates.io-index" 535 + checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" 536 + dependencies = [ 537 + "proc-macro2", 538 + "quote", 539 + "syn", 540 + ] 541 + 542 + [[package]] 327 543 name = "equivalent" 328 544 version = "1.0.2" 329 545 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 340 556 ] 341 557 342 558 [[package]] 559 + name = "exr" 560 + version = "1.74.0" 561 + source = "registry+https://github.com/rust-lang/crates.io-index" 562 + checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" 563 + dependencies = [ 564 + "bit_field", 565 + "half", 566 + "lebe", 567 + "miniz_oxide", 568 + "rayon-core", 569 + "smallvec", 570 + "zune-inflate", 571 + ] 572 + 573 + [[package]] 574 + name = "fax" 575 + version = "0.2.6" 576 + source = "registry+https://github.com/rust-lang/crates.io-index" 577 + checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" 578 + dependencies = [ 579 + "fax_derive", 580 + ] 581 + 582 + [[package]] 583 + name = "fax_derive" 584 + version = "0.2.0" 585 + source = "registry+https://github.com/rust-lang/crates.io-index" 586 + checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" 587 + dependencies = [ 588 + "proc-macro2", 589 + "quote", 590 + "syn", 591 + ] 592 + 593 + [[package]] 594 + name = "fdeflate" 595 + version = "0.3.7" 596 + source = "registry+https://github.com/rust-lang/crates.io-index" 597 + checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 598 + dependencies = [ 599 + "simd-adler32", 600 + ] 601 + 602 + [[package]] 343 603 name = "find-msvc-tools" 344 604 version = "0.1.5" 345 605 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 377 637 ] 378 638 379 639 [[package]] 640 + name = "getrandom" 641 + version = "0.2.16" 642 + source = "registry+https://github.com/rust-lang/crates.io-index" 643 + checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 644 + dependencies = [ 645 + "cfg-if", 646 + "libc", 647 + "wasi", 648 + ] 649 + 650 + [[package]] 651 + name = "getrandom" 652 + version = "0.3.4" 653 + source = "registry+https://github.com/rust-lang/crates.io-index" 654 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 655 + dependencies = [ 656 + "cfg-if", 657 + "libc", 658 + "r-efi", 659 + "wasip2", 660 + ] 661 + 662 + [[package]] 663 + name = "gif" 664 + version = "0.14.1" 665 + source = "registry+https://github.com/rust-lang/crates.io-index" 666 + checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" 667 + dependencies = [ 668 + "color_quant", 669 + "weezl", 670 + ] 671 + 672 + [[package]] 673 + name = "half" 674 + version = "2.7.1" 675 + source = "registry+https://github.com/rust-lang/crates.io-index" 676 + checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" 677 + dependencies = [ 678 + "cfg-if", 679 + "crunchy", 680 + "zerocopy", 681 + ] 682 + 683 + [[package]] 380 684 name = "hashbrown" 381 685 version = "0.15.5" 382 686 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 394 698 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 395 699 396 700 [[package]] 701 + name = "icy_sixel" 702 + version = "0.1.3" 703 + source = "registry+https://github.com/rust-lang/crates.io-index" 704 + checksum = "ccc0a9c4770bc47b0a933256a496cfb8b6531f753ea9bccb19c6dff0ff7273fc" 705 + 706 + [[package]] 397 707 name = "ident_case" 398 708 version = "1.0.1" 399 709 source = "registry+https://github.com/rust-lang/crates.io-index" 400 710 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 401 711 402 712 [[package]] 713 + name = "image" 714 + version = "0.25.9" 715 + source = "registry+https://github.com/rust-lang/crates.io-index" 716 + checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" 717 + dependencies = [ 718 + "bytemuck", 719 + "byteorder-lite", 720 + "color_quant", 721 + "exr", 722 + "gif", 723 + "image-webp", 724 + "moxcms", 725 + "num-traits", 726 + "png", 727 + "qoi", 728 + "ravif", 729 + "rayon", 730 + "rgb", 731 + "tiff", 732 + "zune-core 0.5.0", 733 + "zune-jpeg 0.5.5", 734 + ] 735 + 736 + [[package]] 737 + name = "image-webp" 738 + version = "0.2.4" 739 + source = "registry+https://github.com/rust-lang/crates.io-index" 740 + checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" 741 + dependencies = [ 742 + "byteorder-lite", 743 + "quick-error", 744 + ] 745 + 746 + [[package]] 747 + name = "imgref" 748 + version = "1.12.0" 749 + source = "registry+https://github.com/rust-lang/crates.io-index" 750 + checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" 751 + 752 + [[package]] 403 753 name = "indexmap" 404 754 version = "2.11.4" 405 755 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 429 779 ] 430 780 431 781 [[package]] 782 + name = "interpolate_name" 783 + version = "0.2.4" 784 + source = "registry+https://github.com/rust-lang/crates.io-index" 785 + checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" 786 + dependencies = [ 787 + "proc-macro2", 788 + "quote", 789 + "syn", 790 + ] 791 + 792 + [[package]] 432 793 name = "is_terminal_polyfill" 433 794 version = "1.70.1" 434 795 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 444 805 ] 445 806 446 807 [[package]] 808 + name = "itertools" 809 + version = "0.14.0" 810 + source = "registry+https://github.com/rust-lang/crates.io-index" 811 + checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 812 + dependencies = [ 813 + "either", 814 + ] 815 + 816 + [[package]] 447 817 name = "itoa" 448 818 version = "1.0.15" 449 819 source = "registry+https://github.com/rust-lang/crates.io-index" 450 820 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 451 821 452 822 [[package]] 823 + name = "jobserver" 824 + version = "0.1.34" 825 + source = "registry+https://github.com/rust-lang/crates.io-index" 826 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 827 + dependencies = [ 828 + "getrandom 0.3.4", 829 + "libc", 830 + ] 831 + 832 + [[package]] 453 833 name = "lantern-cli" 454 834 version = "0.1.0" 455 835 dependencies = [ ··· 475 855 "serde_yml", 476 856 "syntect", 477 857 "terminal-colorsaurus", 478 - "thiserror", 858 + "thiserror 2.0.17", 479 859 "toml", 480 860 "tracing", 481 861 "unicode-width 0.2.0", ··· 486 866 version = "0.1.0" 487 867 dependencies = [ 488 868 "crossterm 0.29.0", 869 + "image", 489 870 "lantern-core", 490 871 "owo-colors", 491 872 "ratatui", 873 + "ratatui-image", 492 874 "unicode-width 0.2.0", 493 875 ] 494 876 ··· 499 881 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 500 882 501 883 [[package]] 884 + name = "lebe" 885 + version = "0.5.3" 886 + source = "registry+https://github.com/rust-lang/crates.io-index" 887 + checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" 888 + 889 + [[package]] 502 890 name = "libc" 503 891 version = "0.2.176" 504 892 source = "registry+https://github.com/rust-lang/crates.io-index" 505 893 checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 506 894 507 895 [[package]] 896 + name = "libfuzzer-sys" 897 + version = "0.4.10" 898 + source = "registry+https://github.com/rust-lang/crates.io-index" 899 + checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" 900 + dependencies = [ 901 + "arbitrary", 902 + "cc", 903 + ] 904 + 905 + [[package]] 508 906 name = "libyml" 509 907 version = "0.0.5" 510 908 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 552 950 version = "0.4.28" 553 951 source = "registry+https://github.com/rust-lang/crates.io-index" 554 952 checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 953 + 954 + [[package]] 955 + name = "loop9" 956 + version = "0.1.5" 957 + source = "registry+https://github.com/rust-lang/crates.io-index" 958 + checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" 959 + dependencies = [ 960 + "imgref", 961 + ] 555 962 556 963 [[package]] 557 964 name = "lru" ··· 563 970 ] 564 971 565 972 [[package]] 973 + name = "maybe-rayon" 974 + version = "0.1.1" 975 + source = "registry+https://github.com/rust-lang/crates.io-index" 976 + checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" 977 + dependencies = [ 978 + "cfg-if", 979 + "rayon", 980 + ] 981 + 982 + [[package]] 566 983 name = "memchr" 567 984 version = "2.7.6" 568 985 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 591 1008 ] 592 1009 593 1010 [[package]] 1011 + name = "moxcms" 1012 + version = "0.7.10" 1013 + source = "registry+https://github.com/rust-lang/crates.io-index" 1014 + checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608" 1015 + dependencies = [ 1016 + "num-traits", 1017 + "pxfm", 1018 + ] 1019 + 1020 + [[package]] 1021 + name = "new_debug_unreachable" 1022 + version = "1.0.6" 1023 + source = "registry+https://github.com/rust-lang/crates.io-index" 1024 + checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 1025 + 1026 + [[package]] 1027 + name = "nom" 1028 + version = "8.0.0" 1029 + source = "registry+https://github.com/rust-lang/crates.io-index" 1030 + checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 1031 + dependencies = [ 1032 + "memchr", 1033 + ] 1034 + 1035 + [[package]] 1036 + name = "noop_proc_macro" 1037 + version = "0.3.0" 1038 + source = "registry+https://github.com/rust-lang/crates.io-index" 1039 + checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 1040 + 1041 + [[package]] 594 1042 name = "nu-ansi-term" 595 1043 version = "0.50.1" 596 1044 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 600 1048 ] 601 1049 602 1050 [[package]] 1051 + name = "num-bigint" 1052 + version = "0.4.6" 1053 + source = "registry+https://github.com/rust-lang/crates.io-index" 1054 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 1055 + dependencies = [ 1056 + "num-integer", 1057 + "num-traits", 1058 + ] 1059 + 1060 + [[package]] 603 1061 name = "num-conv" 604 1062 version = "0.1.0" 605 1063 source = "registry+https://github.com/rust-lang/crates.io-index" 606 1064 checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 607 1065 608 1066 [[package]] 1067 + name = "num-derive" 1068 + version = "0.4.2" 1069 + source = "registry+https://github.com/rust-lang/crates.io-index" 1070 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 1071 + dependencies = [ 1072 + "proc-macro2", 1073 + "quote", 1074 + "syn", 1075 + ] 1076 + 1077 + [[package]] 1078 + name = "num-integer" 1079 + version = "0.1.46" 1080 + source = "registry+https://github.com/rust-lang/crates.io-index" 1081 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1082 + dependencies = [ 1083 + "num-traits", 1084 + ] 1085 + 1086 + [[package]] 1087 + name = "num-rational" 1088 + version = "0.4.2" 1089 + source = "registry+https://github.com/rust-lang/crates.io-index" 1090 + checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 1091 + dependencies = [ 1092 + "num-bigint", 1093 + "num-integer", 1094 + "num-traits", 1095 + ] 1096 + 1097 + [[package]] 1098 + name = "num-traits" 1099 + version = "0.2.19" 1100 + source = "registry+https://github.com/rust-lang/crates.io-index" 1101 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1102 + dependencies = [ 1103 + "autocfg", 1104 + ] 1105 + 1106 + [[package]] 609 1107 name = "once_cell" 610 1108 version = "1.21.3" 611 1109 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 640 1138 ] 641 1139 642 1140 [[package]] 1141 + name = "outref" 1142 + version = "0.5.2" 1143 + source = "registry+https://github.com/rust-lang/crates.io-index" 1144 + checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" 1145 + 1146 + [[package]] 643 1147 name = "owo-colors" 644 1148 version = "4.2.3" 645 1149 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 675 1179 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 676 1180 677 1181 [[package]] 1182 + name = "pastey" 1183 + version = "0.1.1" 1184 + source = "registry+https://github.com/rust-lang/crates.io-index" 1185 + checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" 1186 + 1187 + [[package]] 678 1188 name = "pin-project-lite" 679 1189 version = "0.2.16" 680 1190 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 700 1210 ] 701 1211 702 1212 [[package]] 1213 + name = "png" 1214 + version = "0.18.0" 1215 + source = "registry+https://github.com/rust-lang/crates.io-index" 1216 + checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" 1217 + dependencies = [ 1218 + "bitflags", 1219 + "crc32fast", 1220 + "fdeflate", 1221 + "flate2", 1222 + "miniz_oxide", 1223 + ] 1224 + 1225 + [[package]] 703 1226 name = "powerfmt" 704 1227 version = "0.2.0" 705 1228 source = "registry+https://github.com/rust-lang/crates.io-index" 706 1229 checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1230 + 1231 + [[package]] 1232 + name = "ppv-lite86" 1233 + version = "0.2.21" 1234 + source = "registry+https://github.com/rust-lang/crates.io-index" 1235 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1236 + dependencies = [ 1237 + "zerocopy", 1238 + ] 707 1239 708 1240 [[package]] 709 1241 name = "proc-macro2" ··· 715 1247 ] 716 1248 717 1249 [[package]] 1250 + name = "profiling" 1251 + version = "1.0.17" 1252 + source = "registry+https://github.com/rust-lang/crates.io-index" 1253 + checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" 1254 + dependencies = [ 1255 + "profiling-procmacros", 1256 + ] 1257 + 1258 + [[package]] 1259 + name = "profiling-procmacros" 1260 + version = "1.0.17" 1261 + source = "registry+https://github.com/rust-lang/crates.io-index" 1262 + checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" 1263 + dependencies = [ 1264 + "quote", 1265 + "syn", 1266 + ] 1267 + 1268 + [[package]] 718 1269 name = "pulldown-cmark" 719 1270 version = "0.13.0" 720 1271 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 734 1285 checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" 735 1286 736 1287 [[package]] 1288 + name = "pxfm" 1289 + version = "0.1.26" 1290 + source = "registry+https://github.com/rust-lang/crates.io-index" 1291 + checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b" 1292 + dependencies = [ 1293 + "num-traits", 1294 + ] 1295 + 1296 + [[package]] 1297 + name = "qoi" 1298 + version = "0.4.1" 1299 + source = "registry+https://github.com/rust-lang/crates.io-index" 1300 + checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 1301 + dependencies = [ 1302 + "bytemuck", 1303 + ] 1304 + 1305 + [[package]] 1306 + name = "quick-error" 1307 + version = "2.0.1" 1308 + source = "registry+https://github.com/rust-lang/crates.io-index" 1309 + checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 1310 + 1311 + [[package]] 737 1312 name = "quick-xml" 738 1313 version = "0.38.4" 739 1314 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 752 1327 ] 753 1328 754 1329 [[package]] 1330 + name = "r-efi" 1331 + version = "5.3.0" 1332 + source = "registry+https://github.com/rust-lang/crates.io-index" 1333 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1334 + 1335 + [[package]] 1336 + name = "rand" 1337 + version = "0.8.5" 1338 + source = "registry+https://github.com/rust-lang/crates.io-index" 1339 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1340 + dependencies = [ 1341 + "libc", 1342 + "rand_chacha 0.3.1", 1343 + "rand_core 0.6.4", 1344 + ] 1345 + 1346 + [[package]] 1347 + name = "rand" 1348 + version = "0.9.2" 1349 + source = "registry+https://github.com/rust-lang/crates.io-index" 1350 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 1351 + dependencies = [ 1352 + "rand_chacha 0.9.0", 1353 + "rand_core 0.9.3", 1354 + ] 1355 + 1356 + [[package]] 1357 + name = "rand_chacha" 1358 + version = "0.3.1" 1359 + source = "registry+https://github.com/rust-lang/crates.io-index" 1360 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1361 + dependencies = [ 1362 + "ppv-lite86", 1363 + "rand_core 0.6.4", 1364 + ] 1365 + 1366 + [[package]] 1367 + name = "rand_chacha" 1368 + version = "0.9.0" 1369 + source = "registry+https://github.com/rust-lang/crates.io-index" 1370 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1371 + dependencies = [ 1372 + "ppv-lite86", 1373 + "rand_core 0.9.3", 1374 + ] 1375 + 1376 + [[package]] 1377 + name = "rand_core" 1378 + version = "0.6.4" 1379 + source = "registry+https://github.com/rust-lang/crates.io-index" 1380 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1381 + dependencies = [ 1382 + "getrandom 0.2.16", 1383 + ] 1384 + 1385 + [[package]] 1386 + name = "rand_core" 1387 + version = "0.9.3" 1388 + source = "registry+https://github.com/rust-lang/crates.io-index" 1389 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1390 + dependencies = [ 1391 + "getrandom 0.3.4", 1392 + ] 1393 + 1394 + [[package]] 755 1395 name = "ratatui" 756 1396 version = "0.29.0" 757 1397 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 763 1403 "crossterm 0.28.1", 764 1404 "indoc", 765 1405 "instability", 766 - "itertools", 1406 + "itertools 0.13.0", 767 1407 "lru", 768 1408 "paste", 769 1409 "strum", ··· 773 1413 ] 774 1414 775 1415 [[package]] 1416 + name = "ratatui-image" 1417 + version = "8.0.2" 1418 + source = "registry+https://github.com/rust-lang/crates.io-index" 1419 + checksum = "4d2d8ad028fcbb171d83cfdeaf44df17bf0eae3585bdd7f89bc87af98fc71b0e" 1420 + dependencies = [ 1421 + "base64-simd", 1422 + "icy_sixel", 1423 + "image", 1424 + "rand 0.8.5", 1425 + "ratatui", 1426 + "rustix 0.38.44", 1427 + "thiserror 1.0.69", 1428 + "windows", 1429 + ] 1430 + 1431 + [[package]] 1432 + name = "rav1e" 1433 + version = "0.8.1" 1434 + source = "registry+https://github.com/rust-lang/crates.io-index" 1435 + checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" 1436 + dependencies = [ 1437 + "aligned-vec", 1438 + "arbitrary", 1439 + "arg_enum_proc_macro", 1440 + "arrayvec", 1441 + "av-scenechange", 1442 + "av1-grain", 1443 + "bitstream-io", 1444 + "built", 1445 + "cfg-if", 1446 + "interpolate_name", 1447 + "itertools 0.14.0", 1448 + "libc", 1449 + "libfuzzer-sys", 1450 + "log", 1451 + "maybe-rayon", 1452 + "new_debug_unreachable", 1453 + "noop_proc_macro", 1454 + "num-derive", 1455 + "num-traits", 1456 + "paste", 1457 + "profiling", 1458 + "rand 0.9.2", 1459 + "rand_chacha 0.9.0", 1460 + "simd_helpers", 1461 + "thiserror 2.0.17", 1462 + "v_frame", 1463 + "wasm-bindgen", 1464 + ] 1465 + 1466 + [[package]] 1467 + name = "ravif" 1468 + version = "0.12.0" 1469 + source = "registry+https://github.com/rust-lang/crates.io-index" 1470 + checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" 1471 + dependencies = [ 1472 + "avif-serialize", 1473 + "imgref", 1474 + "loop9", 1475 + "quick-error", 1476 + "rav1e", 1477 + "rayon", 1478 + "rgb", 1479 + ] 1480 + 1481 + [[package]] 1482 + name = "rayon" 1483 + version = "1.11.0" 1484 + source = "registry+https://github.com/rust-lang/crates.io-index" 1485 + checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 1486 + dependencies = [ 1487 + "either", 1488 + "rayon-core", 1489 + ] 1490 + 1491 + [[package]] 1492 + name = "rayon-core" 1493 + version = "1.13.0" 1494 + source = "registry+https://github.com/rust-lang/crates.io-index" 1495 + checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 1496 + dependencies = [ 1497 + "crossbeam-deque", 1498 + "crossbeam-utils", 1499 + ] 1500 + 1501 + [[package]] 776 1502 name = "redox_syscall" 777 1503 version = "0.5.18" 778 1504 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 786 1512 version = "0.8.8" 787 1513 source = "registry+https://github.com/rust-lang/crates.io-index" 788 1514 checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 1515 + 1516 + [[package]] 1517 + name = "rgb" 1518 + version = "0.8.52" 1519 + source = "registry+https://github.com/rust-lang/crates.io-index" 1520 + checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" 789 1521 790 1522 [[package]] 791 1523 name = "rustix" ··· 959 1691 checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 960 1692 961 1693 [[package]] 1694 + name = "simd_helpers" 1695 + version = "0.1.0" 1696 + source = "registry+https://github.com/rust-lang/crates.io-index" 1697 + checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" 1698 + dependencies = [ 1699 + "quote", 1700 + ] 1701 + 1702 + [[package]] 962 1703 name = "smallvec" 963 1704 version = "1.15.1" 964 1705 source = "registry+https://github.com/rust-lang/crates.io-index" 965 1706 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1707 + 1708 + [[package]] 1709 + name = "stable_deref_trait" 1710 + version = "1.2.1" 1711 + source = "registry+https://github.com/rust-lang/crates.io-index" 1712 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 966 1713 967 1714 [[package]] 968 1715 name = "static_assertions" ··· 1025 1772 "serde", 1026 1773 "serde_derive", 1027 1774 "serde_json", 1028 - "thiserror", 1775 + "thiserror 2.0.17", 1029 1776 "walkdir", 1030 1777 "yaml-rust", 1031 1778 ] ··· 1058 1805 1059 1806 [[package]] 1060 1807 name = "thiserror" 1808 + version = "1.0.69" 1809 + source = "registry+https://github.com/rust-lang/crates.io-index" 1810 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1811 + dependencies = [ 1812 + "thiserror-impl 1.0.69", 1813 + ] 1814 + 1815 + [[package]] 1816 + name = "thiserror" 1061 1817 version = "2.0.17" 1062 1818 source = "registry+https://github.com/rust-lang/crates.io-index" 1063 1819 checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 1064 1820 dependencies = [ 1065 - "thiserror-impl", 1821 + "thiserror-impl 2.0.17", 1822 + ] 1823 + 1824 + [[package]] 1825 + name = "thiserror-impl" 1826 + version = "1.0.69" 1827 + source = "registry+https://github.com/rust-lang/crates.io-index" 1828 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1829 + dependencies = [ 1830 + "proc-macro2", 1831 + "quote", 1832 + "syn", 1066 1833 ] 1067 1834 1068 1835 [[package]] ··· 1083 1850 checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 1084 1851 dependencies = [ 1085 1852 "cfg-if", 1853 + ] 1854 + 1855 + [[package]] 1856 + name = "tiff" 1857 + version = "0.10.3" 1858 + source = "registry+https://github.com/rust-lang/crates.io-index" 1859 + checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" 1860 + dependencies = [ 1861 + "fax", 1862 + "flate2", 1863 + "half", 1864 + "quick-error", 1865 + "weezl", 1866 + "zune-jpeg 0.4.21", 1086 1867 ] 1087 1868 1088 1869 [[package]] ··· 1236 2017 source = "registry+https://github.com/rust-lang/crates.io-index" 1237 2018 checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1238 2019 dependencies = [ 1239 - "itertools", 2020 + "itertools 0.13.0", 1240 2021 "unicode-segmentation", 1241 2022 "unicode-width 0.1.14", 1242 2023 ] ··· 1260 2041 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1261 2042 1262 2043 [[package]] 2044 + name = "v_frame" 2045 + version = "0.3.9" 2046 + source = "registry+https://github.com/rust-lang/crates.io-index" 2047 + checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" 2048 + dependencies = [ 2049 + "aligned-vec", 2050 + "num-traits", 2051 + "wasm-bindgen", 2052 + ] 2053 + 2054 + [[package]] 1263 2055 name = "valuable" 1264 2056 version = "0.1.1" 1265 2057 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1272 2064 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1273 2065 1274 2066 [[package]] 2067 + name = "vsimd" 2068 + version = "0.8.0" 2069 + source = "registry+https://github.com/rust-lang/crates.io-index" 2070 + checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" 2071 + 2072 + [[package]] 1275 2073 name = "walkdir" 1276 2074 version = "2.5.0" 1277 2075 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1288 2086 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1289 2087 1290 2088 [[package]] 2089 + name = "wasip2" 2090 + version = "1.0.1+wasi-0.2.4" 2091 + source = "registry+https://github.com/rust-lang/crates.io-index" 2092 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 2093 + dependencies = [ 2094 + "wit-bindgen", 2095 + ] 2096 + 2097 + [[package]] 2098 + name = "wasm-bindgen" 2099 + version = "0.2.106" 2100 + source = "registry+https://github.com/rust-lang/crates.io-index" 2101 + checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 2102 + dependencies = [ 2103 + "cfg-if", 2104 + "once_cell", 2105 + "rustversion", 2106 + "wasm-bindgen-macro", 2107 + "wasm-bindgen-shared", 2108 + ] 2109 + 2110 + [[package]] 2111 + name = "wasm-bindgen-macro" 2112 + version = "0.2.106" 2113 + source = "registry+https://github.com/rust-lang/crates.io-index" 2114 + checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" 2115 + dependencies = [ 2116 + "quote", 2117 + "wasm-bindgen-macro-support", 2118 + ] 2119 + 2120 + [[package]] 2121 + name = "wasm-bindgen-macro-support" 2122 + version = "0.2.106" 2123 + source = "registry+https://github.com/rust-lang/crates.io-index" 2124 + checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" 2125 + dependencies = [ 2126 + "bumpalo", 2127 + "proc-macro2", 2128 + "quote", 2129 + "syn", 2130 + "wasm-bindgen-shared", 2131 + ] 2132 + 2133 + [[package]] 2134 + name = "wasm-bindgen-shared" 2135 + version = "0.2.106" 2136 + source = "registry+https://github.com/rust-lang/crates.io-index" 2137 + checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" 2138 + dependencies = [ 2139 + "unicode-ident", 2140 + ] 2141 + 2142 + [[package]] 2143 + name = "weezl" 2144 + version = "0.1.12" 2145 + source = "registry+https://github.com/rust-lang/crates.io-index" 2146 + checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" 2147 + 2148 + [[package]] 1291 2149 name = "winapi" 1292 2150 version = "0.3.9" 1293 2151 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1319 2177 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1320 2178 1321 2179 [[package]] 2180 + name = "windows" 2181 + version = "0.58.0" 2182 + source = "registry+https://github.com/rust-lang/crates.io-index" 2183 + checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" 2184 + dependencies = [ 2185 + "windows-core", 2186 + "windows-targets 0.52.6", 2187 + ] 2188 + 2189 + [[package]] 2190 + name = "windows-core" 2191 + version = "0.58.0" 2192 + source = "registry+https://github.com/rust-lang/crates.io-index" 2193 + checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" 2194 + dependencies = [ 2195 + "windows-implement", 2196 + "windows-interface", 2197 + "windows-result", 2198 + "windows-strings", 2199 + "windows-targets 0.52.6", 2200 + ] 2201 + 2202 + [[package]] 2203 + name = "windows-implement" 2204 + version = "0.58.0" 2205 + source = "registry+https://github.com/rust-lang/crates.io-index" 2206 + checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" 2207 + dependencies = [ 2208 + "proc-macro2", 2209 + "quote", 2210 + "syn", 2211 + ] 2212 + 2213 + [[package]] 2214 + name = "windows-interface" 2215 + version = "0.58.0" 2216 + source = "registry+https://github.com/rust-lang/crates.io-index" 2217 + checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" 2218 + dependencies = [ 2219 + "proc-macro2", 2220 + "quote", 2221 + "syn", 2222 + ] 2223 + 2224 + [[package]] 1322 2225 name = "windows-link" 1323 2226 version = "0.2.1" 1324 2227 source = "registry+https://github.com/rust-lang/crates.io-index" 1325 2228 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 2229 + 2230 + [[package]] 2231 + name = "windows-result" 2232 + version = "0.2.0" 2233 + source = "registry+https://github.com/rust-lang/crates.io-index" 2234 + checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 2235 + dependencies = [ 2236 + "windows-targets 0.52.6", 2237 + ] 2238 + 2239 + [[package]] 2240 + name = "windows-strings" 2241 + version = "0.1.0" 2242 + source = "registry+https://github.com/rust-lang/crates.io-index" 2243 + checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 2244 + dependencies = [ 2245 + "windows-result", 2246 + "windows-targets 0.52.6", 2247 + ] 1326 2248 1327 2249 [[package]] 1328 2250 name = "windows-sys" ··· 1496 2418 checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 1497 2419 1498 2420 [[package]] 2421 + name = "wit-bindgen" 2422 + version = "0.46.0" 2423 + source = "registry+https://github.com/rust-lang/crates.io-index" 2424 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 2425 + 2426 + [[package]] 1499 2427 name = "xterm-color" 1500 2428 version = "1.0.1" 1501 2429 source = "registry+https://github.com/rust-lang/crates.io-index" 1502 2430 checksum = "4de5f056fb9dc8b7908754867544e26145767187aaac5a98495e88ad7cb8a80f" 1503 2431 1504 2432 [[package]] 2433 + name = "y4m" 2434 + version = "0.8.0" 2435 + source = "registry+https://github.com/rust-lang/crates.io-index" 2436 + checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" 2437 + 2438 + [[package]] 1505 2439 name = "yaml-rust" 1506 2440 version = "0.4.5" 1507 2441 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1509 2443 dependencies = [ 1510 2444 "linked-hash-map", 1511 2445 ] 2446 + 2447 + [[package]] 2448 + name = "zerocopy" 2449 + version = "0.8.31" 2450 + source = "registry+https://github.com/rust-lang/crates.io-index" 2451 + checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 2452 + dependencies = [ 2453 + "zerocopy-derive", 2454 + ] 2455 + 2456 + [[package]] 2457 + name = "zerocopy-derive" 2458 + version = "0.8.31" 2459 + source = "registry+https://github.com/rust-lang/crates.io-index" 2460 + checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 2461 + dependencies = [ 2462 + "proc-macro2", 2463 + "quote", 2464 + "syn", 2465 + ] 2466 + 2467 + [[package]] 2468 + name = "zune-core" 2469 + version = "0.4.12" 2470 + source = "registry+https://github.com/rust-lang/crates.io-index" 2471 + checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 2472 + 2473 + [[package]] 2474 + name = "zune-core" 2475 + version = "0.5.0" 2476 + source = "registry+https://github.com/rust-lang/crates.io-index" 2477 + checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" 2478 + 2479 + [[package]] 2480 + name = "zune-inflate" 2481 + version = "0.2.54" 2482 + source = "registry+https://github.com/rust-lang/crates.io-index" 2483 + checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 2484 + dependencies = [ 2485 + "simd-adler32", 2486 + ] 2487 + 2488 + [[package]] 2489 + name = "zune-jpeg" 2490 + version = "0.4.21" 2491 + source = "registry+https://github.com/rust-lang/crates.io-index" 2492 + checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" 2493 + dependencies = [ 2494 + "zune-core 0.4.12", 2495 + ] 2496 + 2497 + [[package]] 2498 + name = "zune-jpeg" 2499 + version = "0.5.5" 2500 + source = "registry+https://github.com/rust-lang/crates.io-index" 2501 + checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e" 2502 + dependencies = [ 2503 + "zune-core 0.5.0", 2504 + ]
+11
README.md
··· 2 2 3 3 > A modern, fast, terminal presentation tool inspired by [`maaslalani/slides`](https://github.com/maaslalani/slides), built with Rust. 4 4 5 + <details> 6 + <summary> 7 + Now with image support (if your terminal supports it!) 8 + </summary> 9 + 10 + ![Rendered on Ghostty](./assets/ghostty.png) 11 + 12 + ![Rendered on iTerm2](./assets/iterm2.png) 13 + 14 + </details> 15 + 5 16 ## Quickstart 6 17 7 18 ### Installation
assets/ghostty.png

This is a binary file and will not be displayed.

assets/iterm2.png

This is a binary file and will not be displayed.

+193 -3
core/src/parser.rs
··· 215 215 ordered: first.is_some(), 216 216 items: Vec::new(), 217 217 current_item: Vec::new(), 218 + pending_nested: None, 218 219 }); 219 220 } 220 221 Tag::BlockQuote(_) => { ··· 255 256 Tag::Strikethrough => { 256 257 current_style.strikethrough = true; 257 258 } 259 + Tag::Image { dest_url, .. } => { 260 + block_stack.push(BlockBuilder::Image { path: dest_url.to_string(), alt: String::new() }); 261 + } 258 262 _ => {} 259 263 }, 260 264 ··· 272 276 TagEnd::List(_) => { 273 277 if let Some(builder) = block_stack.pop() { 274 278 let block = builder.build(); 275 - if let Some(BlockBuilder::Admonition { blocks: adm_blocks, .. }) = block_stack.last_mut() { 279 + 280 + if let Some(BlockBuilder::List { pending_nested, .. }) = block_stack.last_mut() { 281 + if let Block::List(list) = block { 282 + *pending_nested = Some(list); 283 + } 284 + } else if let Some(BlockBuilder::Admonition { blocks: adm_blocks, .. }) = block_stack.last_mut() 285 + { 276 286 adm_blocks.push(block); 277 287 } else { 278 288 blocks.push(block); ··· 320 330 } 321 331 } 322 332 TagEnd::Item => { 323 - if let Some(BlockBuilder::List { current_item, items, .. }) = block_stack.last_mut() { 333 + if let Some(BlockBuilder::List { current_item, items, pending_nested, .. }) = block_stack.last_mut() 334 + { 324 335 if !current_item.is_empty() { 325 - items.push(ListItem { spans: std::mem::take(current_item), nested: None }); 336 + let nested = pending_nested.take().map(Box::new); 337 + items.push(ListItem { spans: std::mem::take(current_item), nested }); 326 338 } 327 339 } 328 340 } ··· 335 347 TagEnd::Strikethrough => { 336 348 current_style.strikethrough = false; 337 349 } 350 + TagEnd::Image => { 351 + if let Some(builder) = block_stack.pop() { 352 + let block = builder.build(); 353 + if let Some(BlockBuilder::Admonition { blocks: adm_blocks, .. }) = block_stack.last_mut() { 354 + adm_blocks.push(block); 355 + } else { 356 + blocks.push(block); 357 + } 358 + } 359 + } 338 360 _ => {} 339 361 }, 340 362 ··· 421 443 ordered: bool, 422 444 items: Vec<ListItem>, 423 445 current_item: Vec<TextSpan>, 446 + pending_nested: Option<List>, 424 447 }, 425 448 BlockQuote { 426 449 blocks: Vec<Block>, ··· 437 460 admonition_type: AdmonitionType, 438 461 title: Option<String>, 439 462 blocks: Vec<Block>, 463 + }, 464 + Image { 465 + path: String, 466 + alt: String, 440 467 }, 441 468 } 442 469 ··· 461 488 current_cell.push(TextSpan { text, style: current_style.clone() }); 462 489 } 463 490 } 491 + Self::Image { alt, .. } => { 492 + alt.push_str(&text); 493 + } 464 494 Self::Admonition { .. } => {} 465 495 _ => {} 466 496 } ··· 493 523 Self::Admonition { admonition_type, title, blocks } => { 494 524 Block::Admonition(Admonition { admonition_type, title, blocks }) 495 525 } 526 + Self::Image { path, alt } => Block::Image { path, alt }, 496 527 } 497 528 } 498 529 } ··· 602 633 } 603 634 604 635 #[test] 636 + fn parse_nested_unordered_list() { 637 + let markdown = "- Item 1\n - Nested 1\n - Nested 2\n- Item 2"; 638 + let slides = parse_slides(markdown).unwrap(); 639 + 640 + match &slides[0].blocks[0] { 641 + Block::List(list) => { 642 + assert!(!list.ordered); 643 + assert_eq!(list.items.len(), 2); 644 + assert_eq!(list.items[0].spans[0].text, "Item 1"); 645 + 646 + let nested = list.items[0].nested.as_ref().expect("Expected nested list"); 647 + assert!(!nested.ordered); 648 + assert_eq!(nested.items.len(), 2); 649 + assert_eq!(nested.items[0].spans[0].text, "Nested 1"); 650 + assert_eq!(nested.items[1].spans[0].text, "Nested 2"); 651 + } 652 + _ => panic!("Expected list"), 653 + } 654 + } 655 + 656 + #[test] 657 + fn parse_nested_ordered_list() { 658 + let markdown = "1. First item\n 1. Nested first\n 2. Nested second\n2. Second item"; 659 + let slides = parse_slides(markdown).unwrap(); 660 + 661 + match &slides[0].blocks[0] { 662 + Block::List(list) => { 663 + assert!(list.ordered); 664 + assert_eq!(list.items.len(), 2); 665 + assert_eq!(list.items[0].spans[0].text, "First item"); 666 + 667 + let nested = list.items[0].nested.as_ref().expect("Expected nested list"); 668 + assert!(nested.ordered); 669 + assert_eq!(nested.items.len(), 2); 670 + assert_eq!(nested.items[0].spans[0].text, "Nested first"); 671 + assert_eq!(nested.items[1].spans[0].text, "Nested second"); 672 + } 673 + _ => panic!("Expected list"), 674 + } 675 + } 676 + 677 + #[test] 678 + fn parse_mixed_nested_list() { 679 + let markdown = "- Unordered item\n 1. Ordered nested\n 2. Another ordered\n- Second unordered"; 680 + let slides = parse_slides(markdown).unwrap(); 681 + 682 + match &slides[0].blocks[0] { 683 + Block::List(list) => { 684 + assert!(!list.ordered); 685 + assert_eq!(list.items.len(), 2); 686 + assert_eq!(list.items[0].spans[0].text, "Unordered item"); 687 + 688 + let nested = list.items[0].nested.as_ref().expect("Expected nested list"); 689 + assert!(nested.ordered); 690 + assert_eq!(nested.items.len(), 2); 691 + assert_eq!(nested.items[0].spans[0].text, "Ordered nested"); 692 + } 693 + _ => panic!("Expected list"), 694 + } 695 + } 696 + 697 + #[test] 698 + fn parse_deeply_nested_list() { 699 + let markdown = "- Level 1\n - Level 2\n - Level 3\n - Back to level 2"; 700 + let slides = parse_slides(markdown).unwrap(); 701 + 702 + match &slides[0].blocks[0] { 703 + Block::List(list) => { 704 + assert!(!list.ordered); 705 + assert_eq!(list.items.len(), 1); 706 + assert_eq!(list.items[0].spans[0].text, "Level 1"); 707 + 708 + let level2 = list.items[0].nested.as_ref().expect("Expected level 2"); 709 + assert_eq!(level2.items.len(), 2); 710 + assert_eq!(level2.items[0].spans[0].text, "Level 2"); 711 + assert_eq!(level2.items[1].spans[0].text, "Back to level 2"); 712 + 713 + let level3 = level2.items[0].nested.as_ref().expect("Expected level 3"); 714 + assert_eq!(level3.items.len(), 1); 715 + assert_eq!(level3.items[0].spans[0].text, "Level 3"); 716 + } 717 + _ => panic!("Expected list"), 718 + } 719 + } 720 + 721 + #[test] 605 722 fn parse_multiple_slides() { 606 723 let markdown = "# Slide 1\nContent 1\n---\n# Slide 2\nContent 2"; 607 724 let slides = parse_slides(markdown).unwrap(); ··· 813 930 fn admonition_type_from_str_invalid() { 814 931 assert!("invalid".parse::<AdmonitionType>().is_err()); 815 932 assert!("".parse::<AdmonitionType>().is_err()); 933 + } 934 + 935 + #[test] 936 + fn parse_image() { 937 + let markdown = "![Test image](path/to/image.png)"; 938 + let slides = parse_slides(markdown).unwrap(); 939 + assert_eq!(slides.len(), 1); 940 + 941 + match &slides[0].blocks[0] { 942 + Block::Image { path, alt } => { 943 + assert_eq!(path, "path/to/image.png"); 944 + assert_eq!(alt, "Test image"); 945 + } 946 + _ => panic!("Expected image block"), 947 + } 948 + } 949 + 950 + #[test] 951 + fn parse_image_no_alt_text() { 952 + let markdown = "![](image.jpg)"; 953 + let slides = parse_slides(markdown).unwrap(); 954 + 955 + match &slides[0].blocks[0] { 956 + Block::Image { path, alt } => { 957 + assert_eq!(path, "image.jpg"); 958 + assert_eq!(alt, ""); 959 + } 960 + _ => panic!("Expected image block"), 961 + } 962 + } 963 + 964 + #[test] 965 + fn parse_image_with_absolute_path() { 966 + let markdown = "![Diagram](/home/user/diagram.svg)"; 967 + let slides = parse_slides(markdown).unwrap(); 968 + 969 + match &slides[0].blocks[0] { 970 + Block::Image { path, alt } => { 971 + assert_eq!(path, "/home/user/diagram.svg"); 972 + assert_eq!(alt, "Diagram"); 973 + } 974 + _ => panic!("Expected image block"), 975 + } 976 + } 977 + 978 + #[test] 979 + fn parse_multiple_images() { 980 + let markdown = "![First](image1.png)\n\n![Second](image2.png)"; 981 + let slides = parse_slides(markdown).unwrap(); 982 + 983 + let image_blocks: Vec<_> = slides[0] 984 + .blocks 985 + .iter() 986 + .filter(|b| matches!(b, Block::Image { .. })) 987 + .collect(); 988 + 989 + assert_eq!(image_blocks.len(), 2); 990 + 991 + match image_blocks[0] { 992 + Block::Image { path, alt } => { 993 + assert_eq!(path, "image1.png"); 994 + assert_eq!(alt, "First"); 995 + } 996 + _ => panic!("Expected image block"), 997 + } 998 + 999 + match image_blocks[1] { 1000 + Block::Image { path, alt } => { 1001 + assert_eq!(path, "image2.png"); 1002 + assert_eq!(alt, "Second"); 1003 + } 1004 + _ => panic!("Expected image block"), 1005 + } 816 1006 } 817 1007 }
+23
core/src/printer.rs
··· 77 77 Block::Admonition(admonition) => { 78 78 print_admonition(writer, admonition, theme, width, indent)?; 79 79 } 80 + Block::Image { path, alt } => { 81 + print_image(writer, path, alt, theme, indent)?; 82 + } 80 83 } 81 84 82 85 Ok(()) ··· 293 296 294 297 let bottom_border = "\u{2570}".to_string() + &"\u{2500}".repeat(box_width.saturating_sub(2)) + "\u{256F}"; 295 298 writeln!(writer, "{}{}", indent_str, color.to_owo_color(&bottom_border))?; 299 + 300 + Ok(()) 301 + } 302 + 303 + /// Print an image placeholder with path and alt text 304 + fn print_image<W: std::io::Write>( 305 + writer: &mut W, path: &str, alt: &str, theme: &ThemeColors, indent: usize, 306 + ) -> std::io::Result<()> { 307 + let indent_str = " ".repeat(indent); 308 + let icon = "\u{1F5BC}"; 309 + 310 + write!(writer, "{indent_str}{}", theme.heading(&format!("{icon} Image: ")))?; 311 + 312 + if !alt.is_empty() { 313 + writeln!(writer, "{}", theme.heading(&alt))?; 314 + } else { 315 + writeln!(writer)?; 316 + } 317 + 318 + writeln!(writer, "{} Path: {}", indent_str, theme.body(&path))?; 296 319 297 320 Ok(()) 298 321 }
+2
core/src/slide.rs
··· 50 50 Table(Table), 51 51 /// Admonition/alert box with type, optional title, and content 52 52 Admonition(Admonition), 53 + /// Image with path and alt text 54 + Image { path: String, alt: String }, 53 55 } 54 56 55 57 /// Styled text span within a block
+2
ui/Cargo.toml
··· 5 5 6 6 [dependencies] 7 7 ratatui = "0.29.0" 8 + ratatui-image = "8.0.2" 9 + image = "0.25" 8 10 crossterm = "0.29.0" 9 11 lantern-core = { path = "../core" } 10 12 owo-colors = "4.2.3"
+121
ui/src/image.rs
··· 1 + use image::DynamicImage; 2 + use ratatui_image::{picker::Picker, protocol::StatefulProtocol}; 3 + use std::collections::HashMap; 4 + use std::io; 5 + use std::path::{Path, PathBuf}; 6 + 7 + /// Manages image loading and protocol state for terminal rendering 8 + /// 9 + /// Handles image loading from paths, protocol detection, and caching of loaded images. 10 + pub struct ImageManager { 11 + picker: Picker, 12 + protocols: HashMap<String, StatefulProtocol>, 13 + base_path: Option<PathBuf>, 14 + } 15 + 16 + impl ImageManager { 17 + /// Create a new ImageManager with protocol detection 18 + pub fn new() -> io::Result<Self> { 19 + let picker = Picker::from_query_stdio().map_err(io::Error::other)?; 20 + 21 + Ok(Self { picker, protocols: HashMap::new(), base_path: None }) 22 + } 23 + 24 + /// Set the base path for resolving relative image paths 25 + pub fn set_base_path(&mut self, path: impl AsRef<Path>) { 26 + self.base_path = Some(path.as_ref().to_path_buf()); 27 + } 28 + 29 + /// Load an image from a path and create a protocol for it 30 + /// 31 + /// Returns a reference to the protocol if successful. 32 + pub fn load_image(&mut self, path: &str) -> io::Result<&mut StatefulProtocol> { 33 + if !self.protocols.contains_key(path) { 34 + let image_path = self.resolve_path(path); 35 + let dyn_img = load_image_from_path(&image_path)?; 36 + let protocol = self.picker.new_resize_protocol(dyn_img); 37 + self.protocols.insert(path.to_string(), protocol); 38 + } 39 + 40 + Ok(self.protocols.get_mut(path).unwrap()) 41 + } 42 + 43 + /// Check if an image is already loaded 44 + pub fn has_image(&self, path: &str) -> bool { 45 + self.protocols.contains_key(path) 46 + } 47 + 48 + /// Get a mutable reference to a loaded image protocol 49 + pub fn get_protocol_mut(&mut self, path: &str) -> Option<&mut StatefulProtocol> { 50 + self.protocols.get_mut(path) 51 + } 52 + 53 + /// Resolve a path relative to the base path if set 54 + fn resolve_path(&self, path: &str) -> PathBuf { 55 + let path = Path::new(path); 56 + 57 + if path.is_absolute() { 58 + return path.to_path_buf(); 59 + } 60 + 61 + if let Some(base) = &self.base_path { 62 + if let Some(parent) = base.parent() { 63 + return parent.join(path); 64 + } 65 + } 66 + 67 + path.to_path_buf() 68 + } 69 + } 70 + 71 + impl Default for ImageManager { 72 + fn default() -> Self { 73 + Self::new().unwrap_or_else(|_| Self { 74 + picker: Picker::from_fontsize((8, 16)), 75 + protocols: HashMap::new(), 76 + base_path: None, 77 + }) 78 + } 79 + } 80 + 81 + /// Load an image from a file path 82 + fn load_image_from_path(path: &Path) -> io::Result<DynamicImage> { 83 + image::ImageReader::open(path) 84 + .map_err(|e| io::Error::new(io::ErrorKind::NotFound, e))? 85 + .decode() 86 + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) 87 + } 88 + 89 + #[cfg(test)] 90 + mod tests { 91 + use super::*; 92 + 93 + #[test] 94 + fn resolve_path_absolute() { 95 + let mut manager = ImageManager::default(); 96 + manager.set_base_path("/home/user/slides.md"); 97 + let resolved = manager.resolve_path("/tmp/image.png"); 98 + assert_eq!(resolved, PathBuf::from("/tmp/image.png")); 99 + } 100 + 101 + #[test] 102 + fn resolve_path_relative() { 103 + let mut manager = ImageManager::default(); 104 + manager.set_base_path("/home/user/slides.md"); 105 + let resolved = manager.resolve_path("images/test.png"); 106 + assert_eq!(resolved, PathBuf::from("/home/user/images/test.png")); 107 + } 108 + 109 + #[test] 110 + fn resolve_path_no_base() { 111 + let manager = ImageManager::default(); 112 + let resolved = manager.resolve_path("test.png"); 113 + assert_eq!(resolved, PathBuf::from("test.png")); 114 + } 115 + 116 + #[test] 117 + fn has_image_returns_false_for_unloaded() { 118 + let manager = ImageManager::default(); 119 + assert!(!manager.has_image("test.png")); 120 + } 121 + }
+3 -1
ui/src/lib.rs
··· 1 1 pub mod app; 2 + pub mod image; 2 3 pub mod layout; 3 4 pub mod renderer; 4 5 pub mod viewer; 5 6 6 7 pub use app::App; 8 + pub use image::ImageManager; 7 9 pub use layout::SlideLayout; 8 - pub use renderer::render_slide_content; 10 + pub use renderer::{ImageInfo, render_slide_content, render_slide_with_images}; 9 11 pub use viewer::SlideViewer; 10 12 11 13 pub use lantern_core::{
+90 -10
ui/src/renderer.rs
··· 9 9 }; 10 10 use unicode_width::UnicodeWidthChar; 11 11 12 + /// Image information extracted from blocks 13 + pub struct ImageInfo { 14 + pub path: String, 15 + pub alt: String, 16 + } 17 + 18 + /// Render a slide's blocks and extract images 19 + /// 20 + /// Returns both the text content and a list of images found in the blocks. 21 + pub fn render_slide_with_images(blocks: &[Block], theme: &ThemeColors) -> (Text<'static>, Vec<ImageInfo>) { 22 + let mut lines = Vec::new(); 23 + let mut images = Vec::new(); 24 + 25 + for block in blocks { 26 + match block { 27 + Block::Heading { level, spans } => render_heading(*level, spans, theme, &mut lines), 28 + Block::Paragraph { spans } => render_paragraph(spans, theme, &mut lines), 29 + Block::Code(code_block) => render_code_block(code_block, theme, &mut lines), 30 + Block::List(list) => render_list(list, theme, &mut lines, 0), 31 + Block::Rule => render_rule(theme, &mut lines), 32 + Block::BlockQuote { blocks } => render_blockquote(blocks, theme, &mut lines), 33 + Block::Table(table) => render_table(table, theme, &mut lines), 34 + Block::Admonition(admonition) => render_admonition(admonition, theme, &mut lines), 35 + Block::Image { path, alt } => images.push(ImageInfo { path: path.clone(), alt: alt.clone() }), 36 + } 37 + 38 + lines.push(Line::raw("")); 39 + } 40 + 41 + (Text::from(lines), images) 42 + } 43 + 12 44 /// Render a slide's blocks into ratatui Text 13 45 /// 14 46 /// Converts slide blocks into styled ratatui text with theming applied. ··· 25 57 Block::BlockQuote { blocks } => render_blockquote(blocks, theme, &mut lines), 26 58 Block::Table(table) => render_table(table, theme, &mut lines), 27 59 Block::Admonition(admonition) => render_admonition(admonition, theme, &mut lines), 60 + // Images are handled separately when using render_slide_with_images 61 + Block::Image { .. } => {} 28 62 } 29 63 30 64 lines.push(Line::raw("")); ··· 34 68 } 35 69 36 70 /// Get heading prefix using Unicode block symbols 71 + /// 1. (*h1*) Large block / heavy fill (`U+2589`) 72 + /// 2. (*h2*) Dark shade (`U+2593`) 73 + /// 3. (*h3*) Medium shade (`U+2592`) 74 + /// 4. (*h4*) Light shade (`U+2591`) 75 + /// 5. (*h5*) Left half block (`U+258C`) 76 + /// 6. (*h6*) Left half block (`U+258C`) 37 77 fn get_prefix(level: u8) -> &'static str { 38 78 match level { 39 - 1 => "▉ ", // Large block / heavy fill (U+2589) 40 - 2 => "▓ ", // Dark shade (U+2593) 41 - 3 => "▒ ", // Medium shade (U+2592) 42 - 4 => "░ ", // Light shade (U+2591) 43 - 5 => "▌ ", // Left half block (U+258C) 44 - _ => "▌ ", // Left half block (U+258C) for h6 79 + 1 => "▉ ", 80 + 2 => "▓ ", 81 + 3 => "▒ ", 82 + 4 => "░ ", 83 + 5 => "▌ ", 84 + _ => "▌ ", 45 85 } 46 86 } 47 87 ··· 116 156 /// Render a horizontal rule 117 157 fn render_rule(theme: &ThemeColors, lines: &mut Vec<Line<'static>>) { 118 158 let rule_style = to_ratatui_style(&theme.rule, false); 119 - let rule = "─".repeat(60); 120 - lines.push(Line::from(Span::styled(rule, rule_style))); 159 + lines.push(Line::from(Span::styled("─".repeat(60), rule_style))); 121 160 } 122 161 123 162 /// Render a blockquote with indentation ··· 367 406 fn to_ratatui_style_converts_color() { 368 407 let color = Color::new(255, 128, 64); 369 408 let style = to_ratatui_style(&color, false); 370 - 371 409 assert_eq!(style.fg, Some(ratatui::style::Color::Rgb(255, 128, 64))); 372 410 } 373 411 ··· 375 413 fn to_ratatui_style_applies_bold() { 376 414 let color = Color::new(100, 150, 200); 377 415 let style = to_ratatui_style(&color, true); 378 - 379 416 assert_eq!(style.fg, Some(ratatui::style::Color::Rgb(100, 150, 200))); 380 417 assert!(style.add_modifier.contains(Modifier::BOLD)); 381 418 } ··· 414 451 style.fg, 415 452 Some(ratatui::style::Color::Rgb(theme.code.r, theme.code.g, theme.code.b)) 416 453 ); 454 + } 455 + 456 + #[test] 457 + fn render_slide_with_images_extracts_image() { 458 + let blocks = 459 + vec![lantern_core::slide::Block::Image { path: "test.png".to_string(), alt: "Test Image".to_string() }]; 460 + let theme = ThemeColors::default(); 461 + let (_text, images) = render_slide_with_images(&blocks, &theme); 462 + 463 + assert_eq!(images.len(), 1); 464 + assert_eq!(images[0].path, "test.png"); 465 + assert_eq!(images[0].alt, "Test Image"); 466 + } 467 + 468 + #[test] 469 + fn render_slide_with_images_extracts_multiple() { 470 + let blocks = vec![ 471 + lantern_core::slide::Block::Image { path: "image1.png".to_string(), alt: "First".to_string() }, 472 + lantern_core::slide::Block::Image { path: "image2.png".to_string(), alt: "Second".to_string() }, 473 + ]; 474 + let theme = ThemeColors::default(); 475 + let (_text, images) = render_slide_with_images(&blocks, &theme); 476 + 477 + assert_eq!(images.len(), 2); 478 + assert_eq!(images[0].path, "image1.png"); 479 + assert_eq!(images[0].alt, "First"); 480 + assert_eq!(images[1].path, "image2.png"); 481 + assert_eq!(images[1].alt, "Second"); 482 + } 483 + 484 + #[test] 485 + fn render_slide_with_mixed_content() { 486 + let blocks = vec![ 487 + lantern_core::slide::Block::Heading { level: 1, spans: vec![TextSpan::plain("Title")] }, 488 + lantern_core::slide::Block::Image { path: "diagram.png".to_string(), alt: "Diagram".to_string() }, 489 + lantern_core::slide::Block::Paragraph { spans: vec![TextSpan::plain("Description")] }, 490 + ]; 491 + let theme = ThemeColors::default(); 492 + let (text, images) = render_slide_with_images(&blocks, &theme); 493 + 494 + assert!(!text.lines.is_empty()); 495 + assert_eq!(images.len(), 1); 496 + assert_eq!(images[0].path, "diagram.png"); 417 497 } 418 498 }
+109 -6
ui/src/viewer.rs
··· 1 1 use lantern_core::{slide::Slide, theme::ThemeColors}; 2 2 use ratatui::{ 3 3 Frame, 4 - layout::Rect, 4 + layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, 5 5 style::{Color, Modifier, Style}, 6 6 text::{Line, Span}, 7 7 widgets::{Block, Borders, Padding, Paragraph, Wrap}, 8 8 }; 9 + use ratatui_image::{Resize, StatefulImage}; 9 10 use std::time::Instant; 10 11 11 - use crate::renderer::render_slide_content; 12 + use crate::image::ImageManager; 13 + use crate::renderer::render_slide_with_images; 12 14 13 15 #[derive(Clone, Copy)] 14 16 struct Stylesheet { ··· 69 71 stylesheet: Stylesheet, 70 72 theme_name: String, 71 73 start_time: Option<Instant>, 74 + image_manager: ImageManager, 72 75 } 73 76 74 77 impl SlideViewer { ··· 82 85 filename: None, 83 86 theme_name: "oxocarbon-dark".to_string(), 84 87 start_time: None, 88 + image_manager: ImageManager::default(), 85 89 } 86 90 } 87 91 ··· 90 94 slides: Vec<Slide>, theme: ThemeColors, filename: Option<String>, theme_name: String, 91 95 start_time: Option<Instant>, 92 96 ) -> Self { 97 + let mut image_manager = ImageManager::default(); 98 + if let Some(ref path) = filename { 99 + image_manager.set_base_path(path); 100 + } 101 + 93 102 Self { 94 103 slides, 95 104 current_index: 0, ··· 98 107 filename, 99 108 theme_name, 100 109 start_time, 110 + image_manager, 101 111 } 102 112 } 103 113 ··· 153 163 } 154 164 155 165 /// Render the current slide to the frame 156 - pub fn render(&self, frame: &mut Frame, area: Rect) { 166 + pub fn render(&mut self, frame: &mut Frame, area: Rect) { 157 167 if let Some(slide) = self.current_slide() { 158 - let content = render_slide_content(&slide.blocks, &self.theme()); 168 + let (content, images) = render_slide_with_images(&slide.blocks, &self.theme()); 159 169 let border_color = self.stylesheet.border_color(); 160 170 let title_color = self.stylesheet.title_color(); 161 171 ··· 166 176 .title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD)) 167 177 .padding(Stylesheet::slide_padding()); 168 178 169 - let paragraph = Paragraph::new(content).block(block).wrap(Wrap { trim: false }); 179 + let inner_area = block.inner(area); 180 + frame.render_widget(block, area); 181 + 182 + let text_height = content.height() as u16; 183 + let mut text_content = Some(content); 184 + 185 + if !images.is_empty() { 186 + let total_images = images.len() as u16; 187 + let border_height_per_image = 1; 188 + let caption_height_per_image = 1; 189 + let min_image_content_height = 1; 190 + let min_height_per_image = 191 + border_height_per_image + min_image_content_height + caption_height_per_image; 192 + let min_images_height = total_images * min_height_per_image; 193 + 194 + let available_height = inner_area.height; 195 + let max_text_height = available_height.saturating_sub(min_images_height); 196 + let text_area_height = text_height.min(max_text_height); 197 + 198 + let chunks = Layout::default() 199 + .direction(Direction::Vertical) 200 + .constraints([Constraint::Length(text_area_height), Constraint::Min(min_images_height)]) 201 + .split(inner_area); 202 + 203 + if chunks[0].height > 0 { 204 + if let Some(text) = text_content.take() { 205 + let paragraph = Paragraph::new(text).wrap(Wrap { trim: false }); 206 + frame.render_widget(paragraph, chunks[0]); 207 + } 208 + } 209 + 210 + let constraints: Vec<Constraint> = (0..total_images) 211 + .map(|_| Constraint::Ratio(1, total_images as u32)) 212 + .collect(); 213 + 214 + let image_chunks = Layout::default() 215 + .direction(Direction::Vertical) 216 + .constraints(constraints) 217 + .split(chunks[1]); 218 + 219 + for (idx, img_info) in images.iter().enumerate() { 220 + if let Ok(protocol) = self.image_manager.load_image(&img_info.path) { 221 + let image_area = image_chunks[idx]; 222 + 223 + let horizontal_chunks = Layout::default() 224 + .direction(Direction::Horizontal) 225 + .constraints([ 226 + Constraint::Percentage(25), 227 + Constraint::Percentage(50), 228 + Constraint::Percentage(25), 229 + ]) 230 + .split(image_area); 231 + 232 + let centered_area = horizontal_chunks[1]; 233 + 234 + let image_block = Block::default() 235 + .borders(Borders::ALL) 236 + .border_style(Style::default().fg(border_color)); 237 + 238 + let image_inner = image_block.inner(centered_area); 239 + frame.render_widget(image_block, centered_area); 240 + 241 + let caption_height = if img_info.alt.is_empty() { 0 } else { 1 }; 242 + let content_chunks = Layout::default() 243 + .direction(Direction::Vertical) 244 + .constraints([Constraint::Length(caption_height), Constraint::Min(1)]) 245 + .flex(Flex::Center) 246 + .split(image_inner); 247 + 248 + if caption_height > 0 { 249 + let caption_style = Style::default() 250 + .fg(Color::Rgb(150, 150, 150)) 251 + .add_modifier(Modifier::ITALIC); 252 + let caption = Paragraph::new(Line::from(Span::styled(&img_info.alt, caption_style))) 253 + .alignment(Alignment::Center); 254 + frame.render_widget(caption, content_chunks[0]); 255 + } 170 256 171 - frame.render_widget(paragraph, area); 257 + let resize = Resize::Fit(None); 258 + let image_size = protocol.size_for(resize, content_chunks[1]); 259 + 260 + let [centered_area] = Layout::horizontal([Constraint::Length(image_size.width)]) 261 + .flex(Flex::Center) 262 + .areas(content_chunks[1]); 263 + let [image_area] = Layout::vertical([Constraint::Length(image_size.height)]) 264 + .flex(Flex::Center) 265 + .areas(centered_area); 266 + 267 + let image_widget = StatefulImage::default(); 268 + frame.render_stateful_widget(image_widget, image_area, protocol); 269 + } 270 + } 271 + } else if let Some(text) = text_content.take() { 272 + let paragraph = Paragraph::new(text).wrap(Wrap { trim: false }); 273 + frame.render_widget(paragraph, inner_area); 274 + } 172 275 } 173 276 } 174 277