calendar: subscribe to external iCal calendars

2c2327e · Gabriel Morell · 2026-10-19 16:35

16 files +1081 -45
Message
{commit_body(@commit)}

Files changed

modified Cargo.lock
+273 −27
@@ -17,6 +17,12 @@ version = "1.0.2"
17 17 source = "registry+https://github.com/rust-lang/crates.io-index"
18 18 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
19 19
20 +[[package]]
21 +name = "adler2"
22 +version = "2.0.1"
23 +source = "registry+https://github.com/rust-lang/crates.io-index"
24 +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
25 +
20 26 [[package]]
21 27 name = "ahash"
22 28 version = "0.7.7"
@@ -200,7 +206,7 @@ dependencies = [
200 206 "cc",
201 207 "cfg-if",
202 208 "libc",
203 − "miniz_oxide",
209 + "miniz_oxide 0.7.1",
204 210 "object",
205 211 "rustc-demangle",
206 212 ]
@@ -211,6 +217,12 @@ version = "0.22.1"
211 217 source = "registry+https://github.com/rust-lang/crates.io-index"
212 218 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
213 219
220 +[[package]]
221 +name = "base64"
222 +version = "0.23.1"
223 +source = "registry+https://github.com/rust-lang/crates.io-index"
224 +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
225 +
214 226 [[package]]
215 227 name = "base64ct"
216 228 version = "1.6.0"
@@ -351,7 +363,7 @@ dependencies = [
351 363 "instant",
352 364 "lazy_static",
353 365 "once_cell",
354 − "thiserror",
366 + "thiserror 1.0.56",
355 367 "tokio",
356 368 ]
357 369
@@ -400,10 +412,11 @@ dependencies = [
400 412
401 413 [[package]]
402 414 name = "cc"
403 −version = "1.2.4"
415 +version = "1.9.0"
404 416 source = "registry+https://github.com/rust-lang/crates.io-index"
405 −checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
417 +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
406 418 dependencies = [
419 + "find-msvc-tools",
407 420 "shlex",
408 421 ]
409 422
@@ -434,6 +447,16 @@ dependencies = [
434 447 "windows-targets 0.52.6",
435 448 ]
436 449
450 +[[package]]
451 +name = "chrono-tz"
452 +version = "0.10.4"
453 +source = "registry+https://github.com/rust-lang/crates.io-index"
454 +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
455 +dependencies = [
456 + "chrono",
457 + "phf",
458 +]
459 +
437 460 [[package]]
438 461 name = "clap"
439 462 version = "4.4.18"
@@ -585,6 +608,15 @@ version = "2.4.0"
585 608 source = "registry+https://github.com/rust-lang/crates.io-index"
586 609 checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
587 610
611 +[[package]]
612 +name = "crc32fast"
613 +version = "1.5.2"
614 +source = "registry+https://github.com/rust-lang/crates.io-index"
615 +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
616 +dependencies = [
617 + "cfg-if",
618 +]
619 +
588 620 [[package]]
589 621 name = "crossbeam-queue"
590 622 version = "0.3.12"
@@ -900,6 +932,23 @@ version = "2.3.0"
900 932 source = "registry+https://github.com/rust-lang/crates.io-index"
901 933 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
902 934
935 +[[package]]
936 +name = "find-msvc-tools"
937 +version = "0.1.15"
938 +source = "registry+https://github.com/rust-lang/crates.io-index"
939 +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
940 +
941 +[[package]]
942 +name = "flate2"
943 +version = "1.1.10"
944 +source = "registry+https://github.com/rust-lang/crates.io-index"
945 +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
946 +dependencies = [
947 + "crc32fast",
948 + "miniz_oxide 0.9.1",
949 + "zlib-rs",
950 +]
951 +
903 952 [[package]]
904 953 name = "flume"
905 954 version = "0.11.1"
@@ -1153,6 +1202,22 @@ dependencies = [
1153 1202 "windows-sys 0.59.0",
1154 1203 ]
1155 1204
1205 +[[package]]
1206 +name = "http"
1207 +version = "1.5.0"
1208 +source = "registry+https://github.com/rust-lang/crates.io-index"
1209 +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
1210 +dependencies = [
1211 + "bytes",
1212 + "itoa",
1213 +]
1214 +
1215 +[[package]]
1216 +name = "httparse"
1217 +version = "1.10.1"
1218 +source = "registry+https://github.com/rust-lang/crates.io-index"
1219 +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
1220 +
1156 1221 [[package]]
1157 1222 name = "humantime"
1158 1223 version = "2.1.0"
@@ -1579,9 +1644,9 @@ dependencies = [
1579 1644
1580 1645 [[package]]
1581 1646 name = "log"
1582 −version = "0.4.22"
1647 +version = "0.4.34"
1583 1648 source = "registry+https://github.com/rust-lang/crates.io-index"
1584 −checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
1649 +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
1585 1650
1586 1651 [[package]]
1587 1652 name = "logos"
@@ -1603,7 +1668,7 @@ dependencies = [
1603 1668 "lazy_static",
1604 1669 "proc-macro2",
1605 1670 "quote",
1606 − "regex-syntax 0.8.2",
1671 + "regex-syntax 0.8.11",
1607 1672 "syn 2.0.90",
1608 1673 ]
1609 1674
@@ -1680,6 +1745,16 @@ dependencies = [
1680 1745 "adler",
1681 1746 ]
1682 1747
1748 +[[package]]
1749 +name = "miniz_oxide"
1750 +version = "0.9.1"
1751 +source = "registry+https://github.com/rust-lang/crates.io-index"
1752 +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
1753 +dependencies = [
1754 + "adler2",
1755 + "simd-adler32",
1756 +]
1757 +
1683 1758 [[package]]
1684 1759 name = "mio"
1685 1760 version = "1.0.3"
@@ -1934,6 +2009,24 @@ version = "2.3.1"
1934 2009 source = "registry+https://github.com/rust-lang/crates.io-index"
1935 2010 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
1936 2011
2012 +[[package]]
2013 +name = "phf"
2014 +version = "0.12.1"
2015 +source = "registry+https://github.com/rust-lang/crates.io-index"
2016 +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
2017 +dependencies = [
2018 + "phf_shared",
2019 +]
2020 +
2021 +[[package]]
2022 +name = "phf_shared"
2023 +version = "0.12.1"
2024 +source = "registry+https://github.com/rust-lang/crates.io-index"
2025 +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
2026 +dependencies = [
2027 + "siphasher",
2028 +]
2029 +
1937 2030 [[package]]
1938 2031 name = "pin-project-lite"
1939 2032 version = "0.2.13"
@@ -1996,6 +2089,7 @@ version = "0.1.0"
1996 2089 dependencies = [
1997 2090 "async-trait",
1998 2091 "chrono",
2092 + "chrono-tz",
1999 2093 "clap",
2000 2094 "color-eyre",
2001 2095 "colored",
@@ -2011,6 +2105,7 @@ dependencies = [
2011 2105 "lazy_static",
2012 2106 "log",
2013 2107 "ratatui",
2108 + "rrule",
2014 2109 "sea-orm",
2015 2110 "sea-orm-migration",
2016 2111 "serde_json",
@@ -2020,6 +2115,7 @@ dependencies = [
2020 2115 "tracing-error",
2021 2116 "tracing-subscriber",
2022 2117 "tui-input",
2118 + "ureq",
2023 2119 "wealthy",
2024 2120 "xdg",
2025 2121 ]
@@ -2209,19 +2305,19 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
2209 2305 dependencies = [
2210 2306 "getrandom",
2211 2307 "libredox",
2212 − "thiserror",
2308 + "thiserror 1.0.56",
2213 2309 ]
2214 2310
2215 2311 [[package]]
2216 2312 name = "regex"
2217 −version = "1.10.3"
2313 +version = "1.13.1"
2218 2314 source = "registry+https://github.com/rust-lang/crates.io-index"
2219 −checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
2315 +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
2220 2316 dependencies = [
2221 2317 "aho-corasick",
2222 2318 "memchr",
2223 − "regex-automata 0.4.4",
2224 − "regex-syntax 0.8.2",
2319 + "regex-automata 0.4.18",
2320 + "regex-syntax 0.8.11",
2225 2321 ]
2226 2322
2227 2323 [[package]]
@@ -2235,13 +2331,13 @@ dependencies = [
2235 2331
2236 2332 [[package]]
2237 2333 name = "regex-automata"
2238 −version = "0.4.4"
2334 +version = "0.4.18"
2239 2335 source = "registry+https://github.com/rust-lang/crates.io-index"
2240 −checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a"
2336 +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
2241 2337 dependencies = [
2242 2338 "aho-corasick",
2243 2339 "memchr",
2244 − "regex-syntax 0.8.2",
2340 + "regex-syntax 0.8.11",
2245 2341 ]
2246 2342
2247 2343 [[package]]
@@ -2252,9 +2348,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
2252 2348
2253 2349 [[package]]
2254 2350 name = "regex-syntax"
2255 −version = "0.8.2"
2351 +version = "0.8.11"
2256 2352 source = "registry+https://github.com/rust-lang/crates.io-index"
2257 −checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
2353 +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
2258 2354
2259 2355 [[package]]
2260 2356 name = "rend"
@@ -2265,6 +2361,20 @@ dependencies = [
2265 2361 "bytecheck",
2266 2362 ]
2267 2363
2364 +[[package]]
2365 +name = "ring"
2366 +version = "0.17.14"
2367 +source = "registry+https://github.com/rust-lang/crates.io-index"
2368 +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
2369 +dependencies = [
2370 + "cc",
2371 + "cfg-if",
2372 + "getrandom",
2373 + "libc",
2374 + "untrusted",
2375 + "windows-sys 0.52.0",
2376 +]
2377 +
2268 2378 [[package]]
2269 2379 name = "rkyv"
2270 2380 version = "0.7.45"
@@ -2294,6 +2404,19 @@ dependencies = [
2294 2404 "syn 1.0.109",
2295 2405 ]
2296 2406
2407 +[[package]]
2408 +name = "rrule"
2409 +version = "0.14.0"
2410 +source = "registry+https://github.com/rust-lang/crates.io-index"
2411 +checksum = "720acfb4980b9d8a6a430f6d7a11933e701ebbeba5eee39cc9d8c5f932aaff74"
2412 +dependencies = [
2413 + "chrono",
2414 + "chrono-tz",
2415 + "log",
2416 + "regex",
2417 + "thiserror 2.0.21",
2418 +]
2419 +
2297 2420 [[package]]
2298 2421 name = "rsa"
2299 2422 version = "0.9.7"
@@ -2349,6 +2472,41 @@ dependencies = [
2349 2472 "windows-sys 0.59.0",
2350 2473 ]
2351 2474
2475 +[[package]]
2476 +name = "rustls"
2477 +version = "0.23.46"
2478 +source = "registry+https://github.com/rust-lang/crates.io-index"
2479 +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
2480 +dependencies = [
2481 + "log",
2482 + "once_cell",
2483 + "ring",
2484 + "rustls-pki-types",
2485 + "rustls-webpki",
2486 + "subtle",
2487 + "zeroize",
2488 +]
2489 +
2490 +[[package]]
2491 +name = "rustls-pki-types"
2492 +version = "1.15.1"
2493 +source = "registry+https://github.com/rust-lang/crates.io-index"
2494 +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
2495 +dependencies = [
2496 + "zeroize",
2497 +]
2498 +
2499 +[[package]]
2500 +name = "rustls-webpki"
2501 +version = "0.103.15"
2502 +source = "registry+https://github.com/rust-lang/crates.io-index"
2503 +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
2504 +dependencies = [
2505 + "ring",
2506 + "rustls-pki-types",
2507 + "untrusted",
2508 +]
2509 +
2352 2510 [[package]]
2353 2511 name = "rustversion"
2354 2512 version = "1.0.14"
@@ -2401,7 +2559,7 @@ dependencies = [
2401 2559 "serde_json",
2402 2560 "sqlx",
2403 2561 "strum",
2404 − "thiserror",
2562 + "thiserror 1.0.56",
2405 2563 "time",
2406 2564 "tracing",
2407 2565 "url",
@@ -2500,7 +2658,7 @@ dependencies = [
2500 2658 "proc-macro2",
2501 2659 "quote",
2502 2660 "syn 2.0.90",
2503 − "thiserror",
2661 + "thiserror 1.0.56",
2504 2662 ]
2505 2663
2506 2664 [[package]]
@@ -2626,9 +2784,9 @@ dependencies = [
2626 2784
2627 2785 [[package]]
2628 2786 name = "shlex"
2629 −version = "1.3.0"
2787 +version = "2.0.1"
2630 2788 source = "registry+https://github.com/rust-lang/crates.io-index"
2631 −checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
2789 +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
2632 2790
2633 2791 [[package]]
2634 2792 name = "signal-hook"
@@ -2670,12 +2828,24 @@ dependencies = [
2670 2828 "rand_core",
2671 2829 ]
2672 2830
2831 +[[package]]
2832 +name = "simd-adler32"
2833 +version = "0.3.10"
2834 +source = "registry+https://github.com/rust-lang/crates.io-index"
2835 +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
2836 +
2673 2837 [[package]]
2674 2838 name = "simdutf8"
2675 2839 version = "0.1.5"
2676 2840 source = "registry+https://github.com/rust-lang/crates.io-index"
2677 2841 checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
2678 2842
2843 +[[package]]
2844 +name = "siphasher"
2845 +version = "1.0.4"
2846 +source = "registry+https://github.com/rust-lang/crates.io-index"
2847 +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
2848 +
2679 2849 [[package]]
2680 2850 name = "slab"
2681 2851 version = "0.4.9"
@@ -2781,7 +2951,7 @@ dependencies = [
2781 2951 "sha2",
2782 2952 "smallvec",
2783 2953 "sqlformat",
2784 − "thiserror",
2954 + "thiserror 1.0.56",
2785 2955 "time",
2786 2956 "tokio",
2787 2957 "tokio-stream",
@@ -2836,7 +3006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2836 3006 checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
2837 3007 dependencies = [
2838 3008 "atoi",
2839 − "base64",
3009 + "base64 0.22.1",
2840 3010 "bigdecimal",
2841 3011 "bitflags 2.4.2",
2842 3012 "byteorder",
@@ -2869,7 +3039,7 @@ dependencies = [
2869 3039 "smallvec",
2870 3040 "sqlx-core",
2871 3041 "stringprep",
2872 − "thiserror",
3042 + "thiserror 1.0.56",
2873 3043 "time",
2874 3044 "tracing",
2875 3045 "uuid",
@@ -2883,7 +3053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2883 3053 checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
2884 3054 dependencies = [
2885 3055 "atoi",
2886 − "base64",
3056 + "base64 0.22.1",
2887 3057 "bigdecimal",
2888 3058 "bitflags 2.4.2",
2889 3059 "byteorder",
@@ -2913,7 +3083,7 @@ dependencies = [
2913 3083 "smallvec",
2914 3084 "sqlx-core",
2915 3085 "stringprep",
2916 − "thiserror",
3086 + "thiserror 1.0.56",
2917 3087 "time",
2918 3088 "tracing",
2919 3089 "uuid",
@@ -3089,7 +3259,16 @@ version = "1.0.56"
3089 3259 source = "registry+https://github.com/rust-lang/crates.io-index"
3090 3260 checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
3091 3261 dependencies = [
3092 − "thiserror-impl",
3262 + "thiserror-impl 1.0.56",
3263 +]
3264 +
3265 +[[package]]
3266 +name = "thiserror"
3267 +version = "2.0.21"
3268 +source = "registry+https://github.com/rust-lang/crates.io-index"
3269 +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
3270 +dependencies = [
3271 + "thiserror-impl 2.0.21",
3093 3272 ]
3094 3273
3095 3274 [[package]]
@@ -3103,6 +3282,17 @@ dependencies = [
3103 3282 "syn 2.0.90",
3104 3283 ]
3105 3284
3285 +[[package]]
3286 +name = "thiserror-impl"
3287 +version = "2.0.21"
3288 +source = "registry+https://github.com/rust-lang/crates.io-index"
3289 +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
3290 +dependencies = [
3291 + "proc-macro2",
3292 + "quote",
3293 + "syn 3.0.9",
3294 +]
3295 +
3106 3296 [[package]]
3107 3297 name = "thread_local"
3108 3298 version = "1.1.8"
@@ -3387,6 +3577,41 @@ version = "0.1.1"
3387 3577 source = "registry+https://github.com/rust-lang/crates.io-index"
3388 3578 checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
3389 3579
3580 +[[package]]
3581 +name = "untrusted"
3582 +version = "0.9.0"
3583 +source = "registry+https://github.com/rust-lang/crates.io-index"
3584 +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
3585 +
3586 +[[package]]
3587 +name = "ureq"
3588 +version = "3.4.2"
3589 +source = "registry+https://github.com/rust-lang/crates.io-index"
3590 +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
3591 +dependencies = [
3592 + "base64 0.23.1",
3593 + "flate2",
3594 + "log",
3595 + "percent-encoding",
3596 + "rustls",
3597 + "rustls-pki-types",
3598 + "ureq-proto",
3599 + "utf8-zero",
3600 + "webpki-roots",
3601 +]
3602 +
3603 +[[package]]
3604 +name = "ureq-proto"
3605 +version = "0.6.5"
3606 +source = "registry+https://github.com/rust-lang/crates.io-index"
3607 +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922"
3608 +dependencies = [
3609 + "base64 0.23.1",
3610 + "http",
3611 + "httparse",
3612 + "log",
3613 +]
3614 +
3390 3615 [[package]]
3391 3616 name = "url"
3392 3617 version = "2.5.4"
@@ -3404,6 +3629,12 @@ version = "1.0.5"
3404 3629 source = "registry+https://github.com/rust-lang/crates.io-index"
3405 3630 checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
3406 3631
3632 +[[package]]
3633 +name = "utf8-zero"
3634 +version = "0.8.1"
3635 +source = "registry+https://github.com/rust-lang/crates.io-index"
3636 +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
3637 +
3407 3638 [[package]]
3408 3639 name = "utf8_iter"
3409 3640 version = "1.0.4"
@@ -3525,6 +3756,15 @@ dependencies = [
3525 3756 "unicode-segmentation",
3526 3757 ]
3527 3758
3759 +[[package]]
3760 +name = "webpki-roots"
3761 +version = "1.0.9"
3762 +source = "registry+https://github.com/rust-lang/crates.io-index"
3763 +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
3764 +dependencies = [
3765 + "rustls-pki-types",
3766 +]
3767 +
3528 3768 [[package]]
3529 3769 name = "whoami"
3530 3770 version = "1.5.2"
@@ -3912,6 +4152,12 @@ dependencies = [
3912 4152 "syn 3.0.9",
3913 4153 ]
3914 4154
4155 +[[package]]
4156 +name = "zlib-rs"
4157 +version = "0.6.8"
4158 +source = "registry+https://github.com/rust-lang/crates.io-index"
4159 +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
4160 +
3915 4161 [[package]]
3916 4162 name = "zmij"
3917 4163 version = "1.0.14"
modified Cargo.toml
+3 −0
@@ -60,6 +60,9 @@ itertools = "0.12.0"
60 60 xdg = "2.5.2"
61 61 serde_json = "1.0.149"
62 62 icu_calendar = "2.3.0"
63 +rrule = "0.14.0"
64 +chrono-tz = "0.10.4"
65 +ureq = "3.4.2"
63 66
64 67
65 68 [lints.clippy]
modified README.md
+19 −0
@@ -138,6 +138,25 @@ ppl calendars show hindu -y 2027 # or from any calendar, any year
138 138
139 139 Turning on a state turns on its country; turning off a country turns off its states.
140 140
141 +#### Subscribed (iCal) calendars
142 +
143 +Any iCal calendar can be added alongside the built-in ones: a Google, Outlook or iCloud calendar's
144 +"secret address in iCal format", a `webcal://` link, or a local `.ics` file.
145 +
146 +```
147 +ppl calendars subscribe work https://calendar.google.com/calendar/ical/.../basic.ics
148 +ppl calendars subscribe family ~/calendars/family.ics
149 +ppl calendars sync # fetch now
150 +ppl calendars unsubscribe work
151 +```
152 +
153 +Subscribed calendars are switched on and off, shown, and reminded of like the built-in ones; their
154 +group for motd defaults is `subscribed` (`ppl calendars set subscribed 1`). Repeating events, moved
155 +or cancelled occurrences and multi-day events are handled; timed events show their local start time.
156 +Remote calendars are cached in `~/.cache/pplapp/ical` and fetched again when they're 6 hours old,
157 +with a 5 second timeout; if that fails (say, offline) the old copy is used. In the TUI, `i` on an
158 +event shows its description, location and link.
159 +
141 160 #### Holidays in the motd
142 161
143 162 `ppl motd` mentions upcoming holidays from the calendars that are on. Like tiers, each calendar has a
modified src/data.rs
+79 −6
@@ -783,13 +783,12 @@ pub async fn delete_editable(db: &DatabaseConnection, editable: &Editable) -> Re
783 783 pub struct CalendarOps {}
784 784
785 785 impl CalendarOps {
786 − /// Keys of the built-in calendars that are switched on
786 + /// Keys of the calendars that are switched on, built-in and subscribed
787 787 pub async fn enabled(db: &DatabaseConnection) -> Result<Vec<String>, DbErr> {
788 − Ok(Calendar::find()
789 − .filter(calendar::Column::Enabled.eq(true))
790 − .all(db)
788 + Ok(Self::rows(db)
791 789 .await?
792 790 .into_iter()
791 + .filter(|c| c.enabled)
793 792 .filter_map(|c| holidays::find(&c.key).map(|d| d.key.to_string()))
794 793 .collect())
795 794 }
@@ -801,6 +800,7 @@ impl CalendarOps {
801 800 key: &str,
802 801 on: bool,
803 802 ) -> Result<Vec<&'static CalendarDef>, PplError> {
803 + Self::rows(db).await?;
804 804 let Some(def) = holidays::find(key) else {
805 805 return Err(invalid(format!(
806 806 "no calendar '{}', see `ppl calendars` for the list",
@@ -824,9 +824,80 @@ impl CalendarOps {
824 824 Ok(changed)
825 825 }
826 826
827 − /// Every row: calendars that have been switched on or given reminder settings, and group defaults
827 + /// Every row: calendars that have been switched on or given reminder settings, group
828 + /// defaults and subscriptions. Reading them makes the subscriptions findable.
828 829 pub async fn rows(db: &DatabaseConnection) -> Result<Vec<calendar::Model>, DbErr> {
829 − Calendar::find().all(db).await
830 + let rows = Calendar::find().all(db).await?;
831 + for r in &rows {
832 + if let Some(url) = &r.url {
833 + holidays::register_subscription(&r.key, url);
834 + }
835 + }
836 + Ok(rows)
837 + }
838 +
839 + /// Subscribed calendars, as (key, url or path)
840 + pub async fn subscriptions(db: &DatabaseConnection) -> Result<Vec<(String, String)>, DbErr> {
841 + Ok(Self::rows(db)
842 + .await?
843 + .into_iter()
844 + .filter_map(|r| Some((r.key, r.url?)))
845 + .collect())
846 + }
847 +
848 + /// Subscribe to an iCal calendar, switched on. It's fetched (or read) first, so a bad
849 + /// URL is refused rather than saved. Returns how many events it has.
850 + pub async fn subscribe(
851 + db: &DatabaseConnection,
852 + name: &str,
853 + source: &str,
854 + ) -> Result<usize, PplError> {
855 + let name = name.trim().to_lowercase();
856 + let valid = !name.is_empty()
857 + && name.len() <= 24
858 + && name
859 + .chars()
860 + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
861 + if !valid {
862 + return Err(invalid(
863 + "a calendar name is up to 24 lowercase letters, digits and dashes, like work or family-ical".to_string(),
864 + ));
865 + }
866 + if holidays::is_builtin(&name) {
867 + return Err(invalid(format!(
868 + "'{}' is a built-in calendar or group; pick another name",
869 + name
870 + )));
871 + }
872 + let rows = Self::rows(db).await?;
873 + if rows.iter().any(|r| r.key == name && r.url.is_some()) {
874 + return Err(invalid(format!(
875 + "already subscribed to '{}'; `ppl calendars unsubscribe {}` first to change it",
876 + name, name
877 + )));
878 + }
879 + let count = crate::ical::sync(&name, source.trim())?;
880 + Self::upsert(db, &name, |row| {
881 + row.url = Set(Some(source.trim().to_string()));
882 + row.enabled = Set(true);
883 + })
884 + .await?;
885 + Self::rows(db).await?;
886 + Ok(count)
887 + }
888 +
889 + pub async fn unsubscribe(db: &DatabaseConnection, name: &str) -> Result<(), PplError> {
890 + let rows = Self::rows(db).await?;
891 + let Some(row) = rows
892 + .into_iter()
893 + .find(|r| r.key.eq_ignore_ascii_case(name) && r.url.is_some())
894 + else {
895 + return Err(invalid(format!("not subscribed to '{}'", name)));
896 + };
897 + Calendar::delete_by_id(row.id).exec(db).await?;
898 + crate::ical::remove_cache(&row.key);
899 + holidays::unregister_subscription(&row.key);
900 + Ok(())
830 901 }
831 902
832 903 /// Set the motd window and mode of a calendar or group; None goes back to the default
@@ -883,6 +954,7 @@ impl CalendarOps {
883 954 sig_date_delta: Set(None),
884 955 sig_remind_enum: Set(None),
885 956 meta: Set(None),
957 + url: Set(None),
886 958 date_ins: Set(today),
887 959 date_up: Set(today),
888 960 },
@@ -1188,6 +1260,7 @@ mod tests {
1188 1260 sig_date_delta: delta,
1189 1261 sig_remind_enum: remind.map(|r| r.to_string()),
1190 1262 meta: None,
1263 + url: None,
1191 1264 date_ins: today,
1192 1265 date_up: today,
1193 1266 }
modified src/do_calendars.rs
+49 −0
@@ -41,6 +41,16 @@ pub enum CalendarAction {
41 41 },
42 42 /// Go back to the defaults (a state's country, then the group, then on the day, daily)
43 43 Clear { target: String },
44 + /// Add an iCal calendar from a URL (http, https, webcal) or an .ics file, switched on
45 + Subscribe {
46 + /// What to call it, like work; lowercase letters, digits and dashes
47 + name: String,
48 + url: String,
49 + },
50 + /// Remove a subscribed calendar and its settings
51 + Unsubscribe { name: String },
52 + /// Fetch subscribed calendars now, rather than when they're 6 hours old
53 + Sync { names: Vec<String> },
44 54 /// Print a year's holidays from the calendars that are on, or the ones named
45 55 Show {
46 56 keys: Vec<String>,
@@ -51,6 +61,10 @@ pub enum CalendarAction {
51 61 }
52 62
53 63 pub async fn do_calendars(args: &CalendarArgs, db: &DatabaseConnection) -> Result<(), PplError> {
64 + let subscriptions = CalendarOps::subscriptions(db).await?;
65 + if matches!(&args.action, Some(CalendarAction::Show { .. })) {
66 + crate::ical::refresh_stale(&subscriptions);
67 + }
54 68 let enabled = CalendarOps::enabled(db).await?;
55 69 let rows = CalendarOps::rows(db).await?;
56 70 match &args.action {
@@ -68,6 +82,41 @@ pub async fn do_calendars(args: &CalendarArgs, db: &DatabaseConnection) -> Resul
68 82 CalendarOps::set_reminder(db, t, r.delta, Some(*mode)).await?;
69 83 println!("{}: remind {}", t.label(), mode.label());
70 84 }
85 + Some(CalendarAction::Subscribe { name, url }) => {
86 + let count = CalendarOps::subscribe(db, name, url).await?;
87 + println!(
88 + "subscribed to {} ({} events); it's on, and refreshes every 6 hours",
89 + name.trim().to_lowercase(),
90 + count
91 + );
92 + }
93 + Some(CalendarAction::Unsubscribe { name }) => {
94 + CalendarOps::unsubscribe(db, name).await?;
95 + println!("unsubscribed from {}", name);
96 + }
97 + Some(CalendarAction::Sync { names }) => {
98 + for n in names {
99 + if !subscriptions.iter().any(|(k, _)| k.eq_ignore_ascii_case(n)) {
100 + return Err(io::Error::new(
101 + io::ErrorKind::NotFound,
102 + format!("not subscribed to '{}'", n),
103 + )
104 + .into());
105 + }
106 + }
107 + if subscriptions.is_empty() {
108 + println!("no subscriptions; add one with `ppl calendars subscribe <name> <url>`");
109 + }
110 + for (key, source) in &subscriptions {
111 + if !names.is_empty() && !names.iter().any(|n| n.eq_ignore_ascii_case(key)) {
112 + continue;
113 + }
114 + match crate::ical::sync(key, source) {
115 + Ok(count) => println!("{}: {} events", key, count),
116 + Err(e) => println!("{}: couldn't fetch, keeping what was there ({})", key, e),
117 + }
118 + }
119 + }
71 120 Some(CalendarAction::Clear { target }) => {
72 121 let t = ReminderTarget::parse(target)?;
73 122 CalendarOps::set_reminder(db, t, None, None).await?;
modified src/do_tui.rs
+7 −3

Click to load diff…

modified src/entities/calendar.rs
+2 −0

Click to load diff…

modified src/holidays.rs
+82 −2

Click to load diff…

added src/ical.rs
+500 −0

Click to load diff…

modified src/main.rs
+3 −0

Click to load diff…

added src/migrator/m20261019_000015_add_url_to_calendar.rs
+43 −0

Click to load diff…

modified src/migrator/mod.rs
+2 −0

Click to load diff…

modified src/tui_calendars.rs
+19 −7

Click to load diff…

Parents: dea84dc