Skip to main content

max / ripgrow

generate: p key pushes workout PDF to a Supernote Adds a `p` binding on the preview screen that generates the session PDF into a temp dir and uploads it via the supernote-push crate to a folder on the tablet configured in ~/Library/Application Support/ripgrow/ config.toml. If the push fails the PDF is kept on disk and the path is surfaced in the status line, so a bad Wi-Fi transfer never loses work. Extracts a `build_session` helper so `e` (export to Downloads) and `p` (export + push) share prescription assembly instead of duplicating it.
Author: Max Johnson <me@maxj.phd> · 2026-07-19 14:05 UTC
Commit: 57a48249d4a75108d872cc7274d18640a3a68a3e
Parent: f6ccabd
6 files changed, +457 insertions, -19 deletions
M Cargo.lock +339 -9
@@ -3,6 +3,12 @@
3 3 version = 4
4 4
5 5 [[package]]
6 + name = "adler2"
7 + version = "2.0.1"
8 + source = "registry+https://github.com/rust-lang/crates.io-index"
9 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10 +
11 + [[package]]
6 12 name = "allocator-api2"
7 13 version = "0.2.21"
8 14 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -24,6 +30,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
24 30 checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
25 31
26 32 [[package]]
33 + name = "base64"
34 + version = "0.22.1"
35 + source = "registry+https://github.com/rust-lang/crates.io-index"
36 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
37 +
38 + [[package]]
39 + name = "bitflags"
40 + version = "1.3.2"
41 + source = "registry+https://github.com/rust-lang/crates.io-index"
42 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
43 +
44 + [[package]]
27 45 name = "bitflags"
28 46 version = "2.13.1"
29 47 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -93,6 +111,15 @@ dependencies = [
93 111 ]
94 112
95 113 [[package]]
114 + name = "concurrent-queue"
115 + version = "2.5.0"
116 + source = "registry+https://github.com/rust-lang/crates.io-index"
117 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
118 + dependencies = [
119 + "crossbeam-utils",
120 + ]
121 +
122 + [[package]]
96 123 name = "convert_case"
97 124 version = "0.10.0"
98 125 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -108,12 +135,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
108 135 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
109 136
110 137 [[package]]
138 + name = "crc32fast"
139 + version = "1.5.0"
140 + source = "registry+https://github.com/rust-lang/crates.io-index"
141 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
142 + dependencies = [
143 + "cfg-if",
144 + ]
145 +
146 + [[package]]
147 + name = "crossbeam-utils"
148 + version = "0.8.22"
149 + source = "registry+https://github.com/rust-lang/crates.io-index"
150 + checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
151 +
152 + [[package]]
111 153 name = "crossterm"
112 154 version = "0.28.1"
113 155 source = "registry+https://github.com/rust-lang/crates.io-index"
114 156 checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
115 157 dependencies = [
116 - "bitflags",
158 + "bitflags 2.13.1",
117 159 "crossterm_winapi",
118 160 "mio",
119 161 "parking_lot",
@@ -129,7 +171,7 @@ version = "0.29.0"
129 171 source = "registry+https://github.com/rust-lang/crates.io-index"
130 172 checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
131 173 dependencies = [
132 - "bitflags",
174 + "bitflags 2.13.1",
133 175 "crossterm_winapi",
134 176 "derive_more",
135 177 "document-features",
@@ -228,6 +270,17 @@ dependencies = [
228 270 ]
229 271
230 272 [[package]]
273 + name = "displaydoc"
274 + version = "0.2.6"
275 + source = "registry+https://github.com/rust-lang/crates.io-index"
276 + checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
277 + dependencies = [
278 + "proc-macro2",
279 + "quote",
280 + "syn",
281 + ]
282 +
283 + [[package]]
231 284 name = "document-features"
232 285 version = "0.2.12"
233 286 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -277,18 +330,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
277 330 checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
278 331
279 332 [[package]]
333 + name = "flate2"
334 + version = "1.1.9"
335 + source = "registry+https://github.com/rust-lang/crates.io-index"
336 + checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
337 + dependencies = [
338 + "crc32fast",
339 + "miniz_oxide",
340 + ]
341 +
342 + [[package]]
343 + name = "flume"
344 + version = "0.11.1"
345 + source = "registry+https://github.com/rust-lang/crates.io-index"
346 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
347 + dependencies = [
348 + "futures-core",
349 + "futures-sink",
350 + "spin",
351 + ]
352 +
353 + [[package]]
280 354 name = "foldhash"
281 355 version = "0.1.5"
282 356 source = "registry+https://github.com/rust-lang/crates.io-index"
283 357 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
284 358
285 359 [[package]]
360 + name = "form_urlencoded"
361 + version = "1.2.2"
362 + source = "registry+https://github.com/rust-lang/crates.io-index"
363 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
364 + dependencies = [
365 + "percent-encoding",
366 + ]
367 +
368 + [[package]]
286 369 name = "futures-core"
287 370 version = "0.3.33"
288 371 source = "registry+https://github.com/rust-lang/crates.io-index"
289 372 checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
290 373
291 374 [[package]]
375 + name = "futures-sink"
376 + version = "0.3.33"
377 + source = "registry+https://github.com/rust-lang/crates.io-index"
378 + checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
379 +
380 + [[package]]
292 381 name = "futures-task"
293 382 version = "0.3.33"
294 383 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -329,12 +418,18 @@ dependencies = [
329 418 ]
330 419
331 420 [[package]]
421 + name = "hashbrown"
422 + version = "0.17.1"
423 + source = "registry+https://github.com/rust-lang/crates.io-index"
424 + checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
425 +
426 + [[package]]
332 427 name = "hashlink"
333 428 version = "0.10.0"
334 429 source = "registry+https://github.com/rust-lang/crates.io-index"
335 430 checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
336 431 dependencies = [
337 - "hashbrown",
432 + "hashbrown 0.15.5",
338 433 ]
339 434
340 435 [[package]]
@@ -368,12 +463,135 @@ dependencies = [
368 463 ]
369 464
370 465 [[package]]
466 + name = "icu_collections"
467 + version = "2.2.0"
468 + source = "registry+https://github.com/rust-lang/crates.io-index"
469 + checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
470 + dependencies = [
471 + "displaydoc",
472 + "potential_utf",
473 + "utf8_iter",
474 + "yoke",
475 + "zerofrom",
476 + "zerovec",
477 + ]
478 +
479 + [[package]]
480 + name = "icu_locale_core"
481 + version = "2.2.0"
482 + source = "registry+https://github.com/rust-lang/crates.io-index"
483 + checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
484 + dependencies = [
485 + "displaydoc",
486 + "litemap",
487 + "tinystr",
488 + "writeable",
489 + "zerovec",
490 + ]
491 +
492 + [[package]]
493 + name = "icu_normalizer"
494 + version = "2.2.0"
495 + source = "registry+https://github.com/rust-lang/crates.io-index"
496 + checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
497 + dependencies = [
498 + "icu_collections",
499 + "icu_normalizer_data",
500 + "icu_properties",
501 + "icu_provider",
502 + "smallvec",
503 + "zerovec",
504 + ]
505 +
506 + [[package]]
507 + name = "icu_normalizer_data"
508 + version = "2.2.0"
509 + source = "registry+https://github.com/rust-lang/crates.io-index"
510 + checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
511 +
512 + [[package]]
513 + name = "icu_properties"
514 + version = "2.2.0"
515 + source = "registry+https://github.com/rust-lang/crates.io-index"
516 + checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
517 + dependencies = [
518 + "icu_collections",
519 + "icu_locale_core",
520 + "icu_properties_data",
521 + "icu_provider",
522 + "zerotrie",
523 + "zerovec",
524 + ]
525 +
526 + [[package]]
527 + name = "icu_properties_data"
528 + version = "2.2.0"
529 + source = "registry+https://github.com/rust-lang/crates.io-index"
530 + checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
531 +
532 + [[package]]
533 + name = "icu_provider"
534 + version = "2.2.0"
535 + source = "registry+https://github.com/rust-lang/crates.io-index"
536 + checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
537 + dependencies = [
538 + "displaydoc",
539 + "icu_locale_core",
540 + "writeable",
541 + "yoke",
542 + "zerofrom",
543 + "zerotrie",
544 + "zerovec",
545 + ]
546 +
547 + [[package]]
371 548 name = "ident_case"
372 549 version = "1.0.1"
373 550 source = "registry+https://github.com/rust-lang/crates.io-index"
374 551 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
375 552
376 553 [[package]]
554 + name = "idna"
555 + version = "1.1.0"
556 + source = "registry+https://github.com/rust-lang/crates.io-index"
557 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
558 + dependencies = [
559 + "idna_adapter",
560 + "smallvec",
561 + "utf8_iter",
562 + ]
563 +
564 + [[package]]
565 + name = "idna_adapter"
566 + version = "1.2.2"
567 + source = "registry+https://github.com/rust-lang/crates.io-index"
568 + checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
569 + dependencies = [
570 + "icu_normalizer",
571 + "icu_properties",
572 + ]
573 +
574 + [[package]]
575 + name = "if-addrs"
576 + version = "0.13.4"
577 + source = "registry+https://github.com/rust-lang/crates.io-index"
578 + checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24"
579 + dependencies = [
580 + "libc",
581 + "windows-sys 0.59.0",
582 + ]
583 +
584 + [[package]]
585 + name = "indexmap"
586 + version = "2.14.0"
587 + source = "registry+https://github.com/rust-lang/crates.io-index"
588 + checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
589 + dependencies = [
590 + "equivalent",
591 + "hashbrown 0.17.1",
592 + ]
593 +
594 + [[package]]
377 595 name = "indoc"
378 596 version = "2.0.7"
379 597 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -460,6 +678,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
460 678 checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
461 679
462 680 [[package]]
681 + name = "litemap"
682 + version = "0.8.2"
683 + source = "registry+https://github.com/rust-lang/crates.io-index"
684 + checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
685 +
686 + [[package]]
463 687 name = "litrs"
464 688 version = "1.0.0"
465 689 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -486,7 +710,36 @@ version = "0.12.5"
486 710 source = "registry+https://github.com/rust-lang/crates.io-index"
487 711 checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
488 712 dependencies = [
489 - "hashbrown",
713 + "hashbrown 0.15.5",
714 + ]
715 +
716 + [[package]]
717 + name = "mdns-sd"
718 + version = "0.11.5"
719 + source = "registry+https://github.com/rust-lang/crates.io-index"
720 + checksum = "8fe7c11a1eb3cfbfcf702d1601c1f5f4c102cdc8665b8a557783ef634741676e"
721 + dependencies = [
722 + "flume",
723 + "if-addrs",
724 + "log",
725 + "polling",
726 + "socket2",
727 + ]
728 +
729 + [[package]]
730 + name = "memchr"
731 + version = "2.8.3"
732 + source = "registry+https://github.com/rust-lang/crates.io-index"
733 + checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
734 +
735 + [[package]]
736 + name = "miniz_oxide"
737 + version = "0.8.9"
738 + source = "registry+https://github.com/rust-lang/crates.io-index"
739 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
740 + dependencies = [
741 + "adler2",
742 + "simd-adler32",
490 743 ]
491 744
492 745 [[package]]
@@ -552,6 +805,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
552 805 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
553 806
554 807 [[package]]
808 + name = "percent-encoding"
809 + version = "2.3.2"
810 + source = "registry+https://github.com/rust-lang/crates.io-index"
811 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
812 +
813 + [[package]]
555 814 name = "pin-project-lite"
556 815 version = "0.2.17"
557 816 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -564,6 +823,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
564 823 checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
565 824
566 825 [[package]]
826 + name = "polling"
827 + version = "2.8.0"
828 + source = "registry+https://github.com/rust-lang/crates.io-index"
829 + checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
830 + dependencies = [
831 + "autocfg",
832 + "bitflags 1.3.2",
833 + "cfg-if",
834 + "concurrent-queue",
835 + "libc",
836 + "log",
837 + "pin-project-lite",
838 + "windows-sys 0.48.0",
839 + ]
840 +
841 + [[package]]
842 + name = "potential_utf"
843 + version = "0.1.5"
844 + source = "registry+https://github.com/rust-lang/crates.io-index"
845 + checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
846 + dependencies = [
847 + "zerovec",
848 + ]
849 +
850 + [[package]]
567 851 name = "proc-macro2"
568 852 version = "1.0.106"
569 853 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -587,7 +871,7 @@ version = "0.29.0"
587 871 source = "registry+https://github.com/rust-lang/crates.io-index"
588 872 checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
589 873 dependencies = [
590 - "bitflags",
874 + "bitflags 2.13.1",
591 875 "cassowary",
592 876 "compact_str",
593 877 "crossterm 0.28.1",
@@ -608,7 +892,7 @@ version = "0.5.18"
608 892 source = "registry+https://github.com/rust-lang/crates.io-index"
609 893 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
610 894 dependencies = [
611 - "bitflags",
895 + "bitflags 2.13.1",
612 896 ]
613 897
614 898 [[package]]
@@ -623,6 +907,20 @@ dependencies = [
623 907 ]
624 908
625 909 [[package]]
910 + name = "ring"
911 + version = "0.17.14"
912 + source = "registry+https://github.com/rust-lang/crates.io-index"
913 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
914 + dependencies = [
915 + "cc",
916 + "cfg-if",
917 + "getrandom",
918 + "libc",
919 + "untrusted",
920 + "windows-sys 0.52.0",
921 + ]
922 +
923 + [[package]]
626 924 name = "ripgrow-core"
627 925 version = "0.1.0"
628 926 dependencies = [
@@ -641,7 +939,10 @@ dependencies = [
641 939 "dirs",
642 940 "ratatui",
643 941 "ripgrow-core",
942 + "serde",
943 + "supernote-push",
644 944 "thiserror",
945 + "toml",
645 946 ]
646 947
647 948 [[package]]
@@ -650,7 +951,7 @@ version = "0.37.0"
650 951 source = "registry+https://github.com/rust-lang/crates.io-index"
651 952 checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
652 953 dependencies = [
653 - "bitflags",
954 + "bitflags 2.13.1",
654 955 "fallible-iterator",
655 956 "fallible-streaming-iterator",
656 957 "hashlink",
@@ -673,7 +974,7 @@ version = "0.38.44"
673 974 source = "registry+https://github.com/rust-lang/crates.io-index"
674 975 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
675 976 dependencies = [
676 - "bitflags",
977 + "bitflags 2.13.1",
677 978 "errno",
678 979 "libc",
679 980 "linux-raw-sys 0.4.15",
@@ -686,7 +987,7 @@ version = "1.1.4"
686 987 source = "registry+https://github.com/rust-lang/crates.io-index"
687 988 checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
688 989 dependencies = [
689 - "bitflags",
990 + "bitflags 2.13.1",
690 991 "errno",
691 992 "libc",
692 993 "linux-raw-sys 0.12.1",
@@ -694,6 +995,41 @@ dependencies = [
694 995 ]
695 996
696 997 [[package]]
998 + name = "rustls"
999 + version = "0.23.42"
1000 + source = "registry+https://github.com/rust-lang/crates.io-index"
1001 + checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
1002 + dependencies = [
1003 + "log",
1004 + "once_cell",
1005 + "ring",
1006 + "rustls-pki-types",
1007 + "rustls-webpki",
1008 + "subtle",
1009 + "zeroize",
1010 + ]
1011 +
1012 + [[package]]
1013 + name = "rustls-pki-types"
1014 + version = "1.15.0"
1015 + source = "registry+https://github.com/rust-lang/crates.io-index"
1016 + checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
1017 + dependencies = [
1018 + "zeroize",
1019 + ]
1020 +
1021 + [[package]]
1022 + name = "rustls-webpki"
1023 + version = "0.103.13"
1024 + source = "registry+https://github.com/rust-lang/crates.io-index"
1025 + checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
1026 + dependencies = [
Lines truncated
M Cargo.toml +3
@@ -16,3 +16,6 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "serd
16 16 dirs = "6"
17 17 ratatui = "0.29"
18 18 crossterm = "0.29"
19 + supernote-push = { path = "../../Libraries/supernote-push" }
20 + serde = { version = "1", features = ["derive"] }
21 + toml = "0.8"
@@ -17,3 +17,6 @@ crossterm = { workspace = true }
17 17 dirs = { workspace = true }
18 18 thiserror = { workspace = true }
19 19 chrono = { workspace = true }
20 + supernote-push = { workspace = true }
21 + serde = { workspace = true }
22 + toml = { workspace = true }
@@ -0,0 +1,39 @@
1 + //! Optional TOML config at `<config_dir>/ripgrow/config.toml`.
2 + //!
3 + //! Currently holds only Supernote push settings. Missing file, missing
4 + //! `[supernote]` table, and unparseable file all degrade to `Config::default()`
5 + //! silently — Supernote is opt-in and its absence is not an error.
6 +
7 + use std::path::PathBuf;
8 +
9 + use serde::Deserialize;
10 +
11 + #[derive(Default, Deserialize)]
12 + pub struct Config {
13 + #[serde(default)]
14 + pub supernote: Option<Supernote>,
15 + }
16 +
17 + #[derive(Deserialize)]
18 + pub struct Supernote {
19 + pub host: String,
20 + #[serde(default = "default_remote_dir")]
21 + pub remote_dir: String,
22 + #[serde(default)]
23 + pub passcode: Option<String>,
24 + }
25 +
26 + fn default_remote_dir() -> String {
27 + "Document/ripgrow".to_string()
28 + }
29 +
30 + pub fn path() -> Option<PathBuf> {
31 + Some(dirs::config_dir()?.join("ripgrow").join("config.toml"))
32 + }
33 +
34 + pub fn load() -> Config {
35 + path()
36 + .and_then(|p| std::fs::read_to_string(p).ok())
37 + .and_then(|s| toml::from_str(&s).ok())
38 + .unwrap_or_default()
39 + }
@@ -3,6 +3,7 @@ use std::time::Duration;
3 3 use crossterm::event::{self, Event};
4 4
5 5 mod app;
6 + mod config;
6 7 mod pdf;
7 8 mod screens;
8 9 mod tui;
@@ -222,15 +222,15 @@ impl GenerateScreen {
222 222 }
223 223 (KeyCode::Char('a'), _) => self.add_slot(db),
224 224 (KeyCode::Char('e'), _) => self.export_pdf(db),
225 + (KeyCode::Char('p'), _) => self.push_supernote(db),
225 226 _ => {}
226 227 }
227 228 }
228 229
229 - fn export_pdf(&mut self, db: &Db) {
230 - let Some(p) = self.preview.as_ref() else { return };
230 + fn build_session(&self, db: &Db) -> Result<SessionExport, String> {
231 + let p = self.preview.as_ref().ok_or_else(|| "no preview".to_string())?;
231 232 if p.slots.is_empty() {
232 - self.status = "no slots to export".to_string();
233 - return;
233 + return Err("no slots to export".to_string());
234 234 }
235 235
236 236 let warmup_ids = warmup_tag_ids(
@@ -259,10 +259,7 @@ impl GenerateScreen {
259 259 Ok(AnyPrescriptionResult::NoHistory) => {
260 260 (PrescriptionParts::no_history(), String::new())
261 261 }
262 - Err(e) => {
263 - self.status = format!("prescription failed: {e}");
264 - return;
265 - }
262 + Err(e) => return Err(format!("prescription failed: {e}")),
266 263 };
267 264 slots.push(SlotExport {
268 265 name: ex.name.clone(),
@@ -274,7 +271,7 @@ impl GenerateScreen {
274 271 });
275 272 }
276 273
277 - let session = SessionExport {
274 + Ok(SessionExport {
278 275 profile: db
279 276 .profile_name()
280 277 .ok()
@@ -283,6 +280,16 @@ impl GenerateScreen {
283 280 date: self.date,
284 281 warmup,
285 282 slots,
283 + })
284 + }
285 +
286 + fn export_pdf(&mut self, db: &Db) {
287 + let session = match self.build_session(db) {
288 + Ok(s) => s,
289 + Err(msg) => {
290 + self.status = msg;
291 + return;
292 + }
286 293 };
287 294
288 295 let output_dir = dirs::download_dir()
@@ -295,6 +302,61 @@ impl GenerateScreen {
295 302 }
296 303 }
297 304
305 + fn push_supernote(&mut self, db: &Db) {
306 + let Some(cfg) = crate::config::load().supernote else {
307 + let hint = crate::config::path()
308 + .map(|p| p.display().to_string())
309 + .unwrap_or_else(|| "config.toml".to_string());
310 + self.status = format!("no [supernote] host in {hint}");
311 + return;
312 + };
313 +
314 + let session = match self.build_session(db) {
315 + Ok(s) => s,
316 + Err(msg) => {
317 + self.status = msg;
318 + return;
319 + }
320 + };
321 +
322 + let tmp = std::env::temp_dir().join("ripgrow-push");
323 + if let Err(e) = std::fs::create_dir_all(&tmp) {
324 + self.status = format!("temp dir failed: {e}");
325 + return;
326 + }
327 +
328 + let pdf_path = match pdf::export_to_pdf(&session, &tmp) {
329 + Ok(p) => p,
330 + Err(e) => {
331 + self.status = format!("export failed: {e}");
332 + return;
333 + }
334 + };
335 + let bytes = match std::fs::read(&pdf_path) {
336 + Ok(b) => b,
337 + Err(e) => {
338 + self.status = format!("read failed: {e}");
339 + return;
340 + }
341 + };
342 + let filename = pdf_path
343 + .file_name()
344 + .and_then(|s| s.to_str())
345 + .unwrap_or("workout.pdf");
346 +
347 + let mut sn = supernote_push::Supernote::at(&cfg.host);
348 + if let Some(pc) = &cfg.passcode {
349 + sn = sn.with_passcode(pc);
350 + }
351 +
352 + match sn.push_pdf(&cfg.remote_dir, filename, &bytes) {
353 + Ok(()) => self.status = format!("pushed: {filename} -> {}", cfg.host),
354 + Err(e) => {
355 + self.status = format!("push failed ({e}); pdf kept at {}", pdf_path.display());
356 + }
357 + }
358 + }
359 +
298 360 fn add_slot(&mut self, db: &Db) {
299 361 let Some(p) = self.preview.as_mut() else { return };
300 362 let days = db.days_since_last_map(self.date).unwrap_or_default();
@@ -358,7 +420,7 @@ impl GenerateScreen {
358 420
359 421 let hint = match &self.preview {
360 422 None => "j/k tag space cycle readiness c clear g generate",
361 - Some(_) => "j/k slot r/R cycle alt d drop a add e export esc back",
423 + Some(_) => "j/k slot r/R cycle alt d drop a add e export p push esc back",
362 424 };
363 425 frame.render_widget(
364 426 Paragraph::new(Line::from(vec![