⭐
Log in to mark this repository.
calendar: built-in holiday calendars, with motd reminders
d9460a8 · Gabriel Morell · 2026-10-19 15:03
✓
Rust CI and Docker Build / Create Release (push)
✓
Rust CI and Docker Build / Release (7z a, .exe, ppl-x86_64-windows.zip, windows-latest, windows-latest, 1.92) (push)
✓
Rust CI and Docker Build / Release (tar -czf, , ppl-aarch64-linux-gnu.tar.gz, ubuntu-24.04-arm, ubuntu-24.04-arm, 1.92) (push)
✓
Rust CI and Docker Build / Release (tar -czf, , ppl-aarch64-macos.tar.gz, macos-latest, macos-latest, 1.92) (push)
✓
Rust CI and Docker Build / Release (tar -czf, , ppl-x86_64-linux-gnu.tar.gz, ubuntu-latest, arc-runner-set, 1.92) (push)
✗
Rust CI and Docker Build / build (aarch64, ubuntu-24.04-arm) (push)
✗
Rust CI and Docker Build / build (x86_64, arc-runner-set) (push)
Message
{commit_body(@commit)}
Files changed
modified
Cargo.lock
+243
−31
@@ -373,6 +373,16 @@ version = "0.1.1"
| 373 | 373 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 374 | 374 | checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" |
| 375 | 375 | |
| 376 | +[[package]] | |
| 377 | +name = "calendrical_calculations" | |
| 378 | +version = "0.2.4" | |
| 379 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 380 | +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" | |
| 381 | +dependencies = [ | |
| 382 | + "core_maths", | |
| 383 | + "displaydoc", | |
| 384 | +] | |
| 385 | + | |
| 376 | 386 | [[package]] |
| 377 | 387 | name = "cassowary" |
| 378 | 388 | version = "0.3.0" |
@@ -542,6 +552,15 @@ version = "0.8.7"
| 542 | 552 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 543 | 553 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" |
| 544 | 554 | |
| 555 | +[[package]] | |
| 556 | +name = "core_maths" | |
| 557 | +version = "0.1.1" | |
| 558 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 559 | +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" | |
| 560 | +dependencies = [ | |
| 561 | + "libm", | |
| 562 | +] | |
| 563 | + | |
| 545 | 564 | [[package]] |
| 546 | 565 | name = "cpufeatures" |
| 547 | 566 | version = "0.2.16" |
@@ -1163,6 +1182,29 @@ dependencies = [
| 1163 | 1182 | "cc", |
| 1164 | 1183 | ] |
| 1165 | 1184 | |
| 1185 | +[[package]] | |
| 1186 | +name = "icu_calendar" | |
| 1187 | +version = "2.3.0" | |
| 1188 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1189 | +checksum = "58655df2f728e46e4eee80bddebefacf52ec3e77cdb925014b47c5a5905eb55c" | |
| 1190 | +dependencies = [ | |
| 1191 | + "calendrical_calculations", | |
| 1192 | + "displaydoc", | |
| 1193 | + "icu_calendar_data", | |
| 1194 | + "icu_locale_core", | |
| 1195 | + "icu_locale_fallback", | |
| 1196 | + "icu_provider 2.3.1", | |
| 1197 | + "ixdtf", | |
| 1198 | + "tinystr 0.8.4", | |
| 1199 | + "zerovec 0.11.8", | |
| 1200 | +] | |
| 1201 | + | |
| 1202 | +[[package]] | |
| 1203 | +name = "icu_calendar_data" | |
| 1204 | +version = "2.3.0" | |
| 1205 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1206 | +checksum = "dc00caaa3fb3201ff7a18aa458e8c3ab042b9da154cfdf948bdde3936c31c36e" | |
| 1207 | + | |
| 1166 | 1208 | [[package]] |
| 1167 | 1209 | name = "icu_collections" |
| 1168 | 1210 | version = "1.5.0" |
@@ -1170,11 +1212,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
| 1170 | 1212 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" |
| 1171 | 1213 | dependencies = [ |
| 1172 | 1214 | "displaydoc", |
| 1173 | − "yoke", | |
| 1215 | + "yoke 0.7.5", | |
| 1174 | 1216 | "zerofrom", |
| 1175 | − "zerovec", | |
| 1217 | + "zerovec 0.10.4", | |
| 1218 | +] | |
| 1219 | + | |
| 1220 | +[[package]] | |
| 1221 | +name = "icu_locale_core" | |
| 1222 | +version = "2.3.0" | |
| 1223 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1224 | +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" | |
| 1225 | +dependencies = [ | |
| 1226 | + "displaydoc", | |
| 1227 | + "litemap 0.8.3", | |
| 1228 | + "serde", | |
| 1229 | + "tinystr 0.8.4", | |
| 1230 | + "writeable 0.6.4", | |
| 1231 | + "zerovec 0.11.8", | |
| 1232 | +] | |
| 1233 | + | |
| 1234 | +[[package]] | |
| 1235 | +name = "icu_locale_fallback" | |
| 1236 | +version = "2.3.0" | |
| 1237 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1238 | +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" | |
| 1239 | +dependencies = [ | |
| 1240 | + "icu_locale_core", | |
| 1241 | + "icu_locale_fallback_data", | |
| 1242 | + "icu_provider 2.3.1", | |
| 1243 | + "potential_utf", | |
| 1244 | + "tinystr 0.8.4", | |
| 1245 | + "zerovec 0.11.8", | |
| 1176 | 1246 | ] |
| 1177 | 1247 | |
| 1248 | +[[package]] | |
| 1249 | +name = "icu_locale_fallback_data" | |
| 1250 | +version = "2.3.0" | |
| 1251 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1252 | +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" | |
| 1253 | + | |
| 1178 | 1254 | [[package]] |
| 1179 | 1255 | name = "icu_locid" |
| 1180 | 1256 | version = "1.5.0" |
@@ -1182,10 +1258,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
| 1182 | 1258 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" |
| 1183 | 1259 | dependencies = [ |
| 1184 | 1260 | "displaydoc", |
| 1185 | − "litemap", | |
| 1186 | − "tinystr", | |
| 1187 | − "writeable", | |
| 1188 | − "zerovec", | |
| 1261 | + "litemap 0.7.4", | |
| 1262 | + "tinystr 0.7.6", | |
| 1263 | + "writeable 0.5.5", | |
| 1264 | + "zerovec 0.10.4", | |
| 1189 | 1265 | ] |
| 1190 | 1266 | |
| 1191 | 1267 | [[package]] |
@@ -1197,9 +1273,9 @@ dependencies = [
| 1197 | 1273 | "displaydoc", |
| 1198 | 1274 | "icu_locid", |
| 1199 | 1275 | "icu_locid_transform_data", |
| 1200 | − "icu_provider", | |
| 1201 | − "tinystr", | |
| 1202 | − "zerovec", | |
| 1276 | + "icu_provider 1.5.0", | |
| 1277 | + "tinystr 0.7.6", | |
| 1278 | + "zerovec 0.10.4", | |
| 1203 | 1279 | ] |
| 1204 | 1280 | |
| 1205 | 1281 | [[package]] |
@@ -1218,12 +1294,12 @@ dependencies = [
| 1218 | 1294 | "icu_collections", |
| 1219 | 1295 | "icu_normalizer_data", |
| 1220 | 1296 | "icu_properties", |
| 1221 | − "icu_provider", | |
| 1297 | + "icu_provider 1.5.0", | |
| 1222 | 1298 | "smallvec", |
| 1223 | 1299 | "utf16_iter", |
| 1224 | 1300 | "utf8_iter", |
| 1225 | 1301 | "write16", |
| 1226 | − "zerovec", | |
| 1302 | + "zerovec 0.10.4", | |
| 1227 | 1303 | ] |
| 1228 | 1304 | |
| 1229 | 1305 | [[package]] |
@@ -1242,9 +1318,9 @@ dependencies = [
| 1242 | 1318 | "icu_collections", |
| 1243 | 1319 | "icu_locid_transform", |
| 1244 | 1320 | "icu_properties_data", |
| 1245 | − "icu_provider", | |
| 1246 | − "tinystr", | |
| 1247 | − "zerovec", | |
| 1321 | + "icu_provider 1.5.0", | |
| 1322 | + "tinystr 0.7.6", | |
| 1323 | + "zerovec 0.10.4", | |
| 1248 | 1324 | ] |
| 1249 | 1325 | |
| 1250 | 1326 | [[package]] |
@@ -1263,11 +1339,28 @@ dependencies = [
| 1263 | 1339 | "icu_locid", |
| 1264 | 1340 | "icu_provider_macros", |
| 1265 | 1341 | "stable_deref_trait", |
| 1266 | − "tinystr", | |
| 1267 | − "writeable", | |
| 1268 | − "yoke", | |
| 1342 | + "tinystr 0.7.6", | |
| 1343 | + "writeable 0.5.5", | |
| 1344 | + "yoke 0.7.5", | |
| 1345 | + "zerofrom", | |
| 1346 | + "zerovec 0.10.4", | |
| 1347 | +] | |
| 1348 | + | |
| 1349 | +[[package]] | |
| 1350 | +name = "icu_provider" | |
| 1351 | +version = "2.3.1" | |
| 1352 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1353 | +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" | |
| 1354 | +dependencies = [ | |
| 1355 | + "displaydoc", | |
| 1356 | + "icu_locale_core", | |
| 1357 | + "serde", | |
| 1358 | + "stable_deref_trait", | |
| 1359 | + "writeable 0.6.4", | |
| 1360 | + "yoke 0.8.3", | |
| 1269 | 1361 | "zerofrom", |
| 1270 | − "zerovec", | |
| 1362 | + "zerotrie", | |
| 1363 | + "zerovec 0.11.8", | |
| 1271 | 1364 | ] |
| 1272 | 1365 | |
| 1273 | 1366 | [[package]] |
@@ -1398,6 +1491,12 @@ version = "1.0.14"
| 1398 | 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1399 | 1492 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" |
| 1400 | 1493 | |
| 1494 | +[[package]] | |
| 1495 | +name = "ixdtf" | |
| 1496 | +version = "0.6.6" | |
| 1497 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1498 | +checksum = "4d3667095d64c3ecffc96463a21157b04bf3e252f6e8d5750b20c02e33c194e3" | |
| 1499 | + | |
| 1401 | 1500 | [[package]] |
| 1402 | 1501 | name = "js-sys" |
| 1403 | 1502 | version = "0.3.76" |
@@ -1462,6 +1561,12 @@ version = "0.7.4"
| 1462 | 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1463 | 1562 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" |
| 1464 | 1563 | |
| 1564 | +[[package]] | |
| 1565 | +name = "litemap" | |
| 1566 | +version = "0.8.3" | |
| 1567 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1568 | +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" | |
| 1569 | + | |
| 1465 | 1570 | [[package]] |
| 1466 | 1571 | name = "lock_api" |
| 1467 | 1572 | version = "0.4.11" |
@@ -1868,6 +1973,17 @@ version = "0.3.29"
| 1868 | 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1869 | 1974 | checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" |
| 1870 | 1975 | |
| 1976 | +[[package]] | |
| 1977 | +name = "potential_utf" | |
| 1978 | +version = "0.1.6" | |
| 1979 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1980 | +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" | |
| 1981 | +dependencies = [ | |
| 1982 | + "serde_core", | |
| 1983 | + "writeable 0.6.4", | |
| 1984 | + "zerovec 0.11.8", | |
| 1985 | +] | |
| 1986 | + | |
| 1871 | 1987 | [[package]] |
| 1872 | 1988 | name = "powerfmt" |
| 1873 | 1989 | version = "0.2.0" |
@@ -1889,6 +2005,7 @@ dependencies = [
| 1889 | 2005 | "enum-iterator", |
| 1890 | 2006 | "env_logger", |
| 1891 | 2007 | "futures", |
| 2008 | + "icu_calendar", | |
| 1892 | 2009 | "interim", |
| 1893 | 2010 | "itertools 0.12.0", |
| 1894 | 2011 | "lazy_static", |
@@ -2001,9 +2118,9 @@ dependencies = [
| 2001 | 2118 | |
| 2002 | 2119 | [[package]] |
| 2003 | 2120 | name = "quote" |
| 2004 | −version = "1.0.37" | |
| 2121 | +version = "1.0.47" | |
| 2005 | 2122 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2006 | −checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" | |
| 2123 | +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" | |
| 2007 | 2124 | dependencies = [ |
| 2008 | 2125 | "proc-macro2", |
| 2009 | 2126 | ] |
@@ -2914,6 +3031,17 @@ dependencies = [
| 2914 | 3031 | "unicode-ident", |
| 2915 | 3032 | ] |
| 2916 | 3033 | |
| 3034 | +[[package]] | |
| 3035 | +name = "syn" | |
| 3036 | +version = "3.0.9" | |
| 3037 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3038 | +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" | |
| 3039 | +dependencies = [ | |
| 3040 | + "proc-macro2", | |
| 3041 | + "quote", | |
| 3042 | + "unicode-ident", | |
| 3043 | +] | |
| 3044 | + | |
| 2917 | 3045 | [[package]] |
| 2918 | 3046 | name = "synstructure" |
| 2919 | 3047 | version = "0.13.1" |
@@ -2925,6 +3053,17 @@ dependencies = [
| 2925 | 3053 | "syn 2.0.90", |
| 2926 | 3054 | ] |
| 2927 | 3055 | |
| 3056 | +[[package]] | |
| 3057 | +name = "synstructure" | |
| 3058 | +version = "0.14.0" | |
| 3059 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3060 | +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" | |
| 3061 | +dependencies = [ | |
| 3062 | + "proc-macro2", | |
| 3063 | + "quote", | |
| 3064 | + "syn 3.0.9", | |
| 3065 | +] | |
| 3066 | + | |
| 2928 | 3067 | [[package]] |
| 2929 | 3068 | name = "tap" |
| 2930 | 3069 | version = "1.0.1" |
@@ -3014,7 +3153,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
| 3014 | 3153 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" |
| 3015 | 3154 | dependencies = [ |
| 3016 | 3155 | "displaydoc", |
| 3017 | − "zerovec", | |
| 3156 | + "zerovec 0.10.4", | |
| 3157 | +] | |
| 3158 | + | |
| 3159 | +[[package]] | |
| 3160 | +name = "tinystr" | |
| 3161 | +version = "0.8.4" | |
| 3162 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3163 | +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" | |
| 3164 | +dependencies = [ | |
| 3165 | + "displaydoc", | |
| 3166 | + "serde_core", | |
| 3167 | + "zerovec 0.11.8", | |
| 3018 | 3168 | ] |
| 3019 | 3169 | |
| 3020 | 3170 | [[package]] |
@@ -3585,6 +3735,12 @@ version = "0.5.5"
| 3585 | 3735 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3586 | 3736 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" |
| 3587 | 3737 | |
| 3738 | +[[package]] | |
| 3739 | +name = "writeable" | |
| 3740 | +version = "0.6.4" | |
| 3741 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3742 | +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" | |
| 3743 | + | |
| 3588 | 3744 | [[package]] |
| 3589 | 3745 | name = "wyz" |
| 3590 | 3746 | version = "0.5.1" |
@@ -3614,7 +3770,18 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
| 3614 | 3770 | dependencies = [ |
| 3615 | 3771 | "serde", |
| 3616 | 3772 | "stable_deref_trait", |
| 3617 | − "yoke-derive", | |
| 3773 | + "yoke-derive 0.7.5", | |
| 3774 | + "zerofrom", | |
| 3775 | +] | |
| 3776 | + | |
| 3777 | +[[package]] | |
| 3778 | +name = "yoke" | |
| 3779 | +version = "0.8.3" | |
| 3780 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3781 | +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" | |
| 3782 | +dependencies = [ | |
| 3783 | + "stable_deref_trait", | |
| 3784 | + "yoke-derive 0.8.4", | |
| 3618 | 3785 | "zerofrom", |
| 3619 | 3786 | ] |
| 3620 | 3787 |
@@ -3627,7 +3794,19 @@ dependencies = [
| 3627 | 3794 | "proc-macro2", |
| 3628 | 3795 | "quote", |
| 3629 | 3796 | "syn 2.0.90", |
| 3630 | − "synstructure", | |
| 3797 | + "synstructure 0.13.1", | |
| 3798 | +] | |
| 3799 | + | |
| 3800 | +[[package]] | |
| 3801 | +name = "yoke-derive" | |
| 3802 | +version = "0.8.4" | |
| 3803 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3804 | +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" | |
| 3805 | +dependencies = [ | |
| 3806 | + "proc-macro2", | |
| 3807 | + "quote", | |
| 3808 | + "syn 3.0.9", | |
| 3809 | + "synstructure 0.14.0", | |
| 3631 | 3810 | ] |
| 3632 | 3811 | |
| 3633 | 3812 | [[package]] |
@@ -3653,23 +3832,23 @@ dependencies = [
| 3653 | 3832 | |
| 3654 | 3833 | [[package]] |
| 3655 | 3834 | name = "zerofrom" |
| 3656 | −version = "0.1.5" | |
| 3835 | +version = "0.1.8" | |
| 3657 | 3836 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3658 | −checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" | |
| 3837 | +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" | |
| 3659 | 3838 | dependencies = [ |
| 3660 | 3839 | "zerofrom-derive", |
| 3661 | 3840 | ] |
| 3662 | 3841 | |
| 3663 | 3842 | [[package]] |
| 3664 | 3843 | name = "zerofrom-derive" |
| 3665 | −version = "0.1.5" | |
| 3844 | +version = "0.1.8" | |
| 3666 | 3845 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3667 | −checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" | |
| 3846 | +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" | |
| 3668 | 3847 | dependencies = [ |
| 3669 | 3848 | "proc-macro2", |
| 3670 | 3849 | "quote", |
| 3671 | − "syn 2.0.90", | |
| 3672 | − "synstructure", | |
| 3850 | + "syn 3.0.9", | |
| 3851 | + "synstructure 0.14.0", | |
| 3673 | 3852 | ] |
| 3674 | 3853 | |
| 3675 | 3854 | [[package]] |
@@ -3678,15 +3857,37 @@ version = "1.8.1"
| 3678 | 3857 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3679 | 3858 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" |
| 3680 | 3859 | |
| 3860 | +[[package]] | |
| 3861 | +name = "zerotrie" | |
| 3862 | +version = "0.2.5" | |
| 3863 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3864 | +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" | |
| 3865 | +dependencies = [ | |
| 3866 | + "displaydoc", | |
| 3867 | + "zerovec 0.11.8", | |
| 3868 | +] | |
| 3869 | + | |
| 3681 | 3870 | [[package]] |
| 3682 | 3871 | name = "zerovec" |
| 3683 | 3872 | version = "0.10.4" |
| 3684 | 3873 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3685 | 3874 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" |
| 3686 | 3875 | dependencies = [ |
| 3687 | − "yoke", | |
| 3876 | + "yoke 0.7.5", | |
| 3688 | 3877 | "zerofrom", |
| 3689 | − "zerovec-derive", | |
| 3878 | + "zerovec-derive 0.10.3", | |
| 3879 | +] | |
| 3880 | + | |
| 3881 | +[[package]] | |
| 3882 | +name = "zerovec" | |
| 3883 | +version = "0.11.8" | |
| 3884 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3885 | +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" | |
| 3886 | +dependencies = [ | |
| 3887 | + "serde", | |
| 3888 | + "yoke 0.8.3", | |
| 3889 | + "zerofrom", | |
| 3890 | + "zerovec-derive 0.11.6", | |
| 3690 | 3891 | ] |
| 3691 | 3892 | |
| 3692 | 3893 | [[package]] |
@@ -3700,6 +3901,17 @@ dependencies = [
| 3700 | 3901 | "syn 2.0.90", |
| 3701 | 3902 | ] |
| 3702 | 3903 | |
| 3904 | +[[package]] | |
| 3905 | +name = "zerovec-derive" | |
| 3906 | +version = "0.11.6" | |
| 3907 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3908 | +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" | |
| 3909 | +dependencies = [ | |
| 3910 | + "proc-macro2", | |
| 3911 | + "quote", | |
| 3912 | + "syn 3.0.9", | |
| 3913 | +] | |
| 3914 | + | |
| 3703 | 3915 | [[package]] |
| 3704 | 3916 | name = "zmij" |
| 3705 | 3917 | version = "1.0.14" |
modified
Cargo.toml
+1
−0
@@ -59,6 +59,7 @@ time = "0.3.37"
| 59 | 59 | itertools = "0.12.0" |
| 60 | 60 | xdg = "2.5.2" |
| 61 | 61 | serde_json = "1.0.149" |
| 62 | +icu_calendar = "2.3.0" | |
| 62 | 63 | |
| 63 | 64 | |
| 64 | 65 | [lints.clippy] |
modified
README.md
+53
−2
@@ -70,7 +70,13 @@ While editing a person, `Delete` (or backspace) twice removes the selected row.
| 70 | 70 | |
| 71 | 71 | #### Calendar View |
| 72 | 72 | |
| 73 | −Upcoming Anniversaries | |
| 73 | +Upcoming anniversaries and holidays for the year; `↑`/`↓` scroll the upcoming list, which opens on today, | |
| 74 | +and the selected day is highlighted in white in the month grid. Press `i` on a holiday for a short | |
| 75 | +description and a Wikipedia link (`o` opens it in your browser). | |
| 76 | + | |
| 77 | +Press `c` to switch built-in calendars on and off. Their holidays show in the upcoming list and | |
| 78 | +colour the month grid: cyan for countries, violet for religious calendars, amber for moon and fiscal. | |
| 79 | +A country's states and provinces appear under it once it's on. | |
| 74 | 80 | |
| 75 | 81 |  |
| 76 | 82 |
@@ -102,4 +108,49 @@ ppl traits add pronouns --symbol 🏳️ --color violet # new trait
| 102 | 108 | ppl traits add namesday --kind date # new kind of date |
| 103 | 109 | ``` |
| 104 | 110 | |
| 105 | −Colors: red, gold, amber, green, teal, cyan, blue, violet, pink, slate. | |
| 111 | +Colors: red, gold, amber, green, teal, cyan, blue, violet, pink, slate. | |
| 112 | + | |
| 113 | +### Calendars | |
| 114 | + | |
| 115 | +Built-in holiday calendars, all computed locally (no downloads, any year): | |
| 116 | + | |
| 117 | +- Countries: United States, Canada, United Kingdom, Ireland, Germany, France, Mexico, Australia, | |
| 118 | + India, Thailand, plus states and provinces for the US, Canada, the UK, Germany and Australia | |
| 119 | + (`ppl calendars list --all`), and US observances like Mother's Day and Halloween | |
| 120 | +- Religious and cultural: Christian (Western and Orthodox), Jewish, Islamic, Chinese (lunar), | |
| 121 | + Persian, Hindu, Buddhist | |
| 122 | +- Moon and fiscal: moon phases with times and full-moon names, and fiscal quarters for years | |
| 123 | + starting January, April, July or October, plus the retail 4-5-4 calendar | |
| 124 | + | |
| 125 | +``` | |
| 126 | +ppl calendars # what's on | |
| 127 | +ppl calendars enable us us-ny jewish lunar | |
| 128 | +ppl calendars disable jewish | |
| 129 | +ppl calendars show # this year's holidays from the calendars that are on | |
| 130 | +ppl calendars show hindu -y 2027 # or from any calendar, any year | |
| 131 | +``` | |
| 132 | + | |
| 133 | +Turning on a state turns on its country; turning off a country turns off its states. | |
| 134 | + | |
| 135 | +#### Holidays in the motd | |
| 136 | + | |
| 137 | +`ppl motd` mentions upcoming holidays from the calendars that are on. Like tiers, each calendar has a | |
| 138 | +window (how many days ahead) and a remind mode (`daily`, `hourly`, `random:N`, `always`, `never`), | |
| 139 | +and each group (`countries`, `religious`, `other` for moon and fiscal) has defaults for its calendars. | |
| 140 | +A calendar uses its own setting, then its country's (for a state), then its group's, then on the day, daily. | |
| 141 | + | |
| 142 | +``` | |
| 143 | +ppl calendars set countries 0 # national holidays on the day | |
| 144 | +ppl calendars set religious 7 # religious ones a week ahead | |
| 145 | +ppl calendars remind religious always | |
| 146 | +ppl calendars set us-ny 3 # but New York's three days ahead | |
| 147 | +ppl calendars clear us-ny # back to the defaults | |
| 148 | +``` | |
| 149 | + | |
| 150 | +In the TUI's calendar popup the same keys as the tiers tab work on a calendar or a group heading: | |
| 151 | +`+`/`-` days ahead, `m` mode, `[`/`]` random %, `x` back to the default. | |
| 152 | + | |
| 153 | +Jewish and Islamic days begin at sundown the evening before. Islamic dates use the Umm al-Qura | |
| 154 | +calendar, and local moon sighting can move them a day. Hindu dates are worked out for India | |
| 155 | +from the positions of the Sun and Moon, and regional calendars can differ by a day. Thailand's | |
| 156 | +Buddhist days follow the astronomical full moon, so they can be a day off the Thai almanac. |
added
src/astro.rs
+226
−0
@@ -0,0 +1,226 @@
| 1 | +//! Just enough astronomy for lunar calendars: where the Sun and Moon are, and | |
| 2 | +//! when the Moon reaches a phase. Formulas are the shorter series from Jean | |
| 3 | +//! Meeus' "Astronomical Algorithms", good to a few minutes of time. | |
| 4 | + | |
| 5 | +use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, Utc}; | |
| 6 | + | |
| 7 | +/// Julian day of a UTC time | |
| 8 | +pub fn jd(t: NaiveDateTime) -> f64 { | |
| 9 | + let epoch = NaiveDate::from_ymd_opt(2000, 1, 1) | |
| 10 | + .and_then(|d| d.and_hms_opt(12, 0, 0)) | |
| 11 | + .unwrap_or_default(); | |
| 12 | + 2451545.0 + (t - epoch).num_seconds() as f64 / 86400.0 | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// UTC time of a Julian day | |
| 16 | +pub fn utc(jd: f64) -> DateTime<Utc> { | |
| 17 | + let secs = ((jd - 2451545.0) * 86400.0).round() as i64; | |
| 18 | + DateTime::<Utc>::from_timestamp(946728000 + secs, 0).unwrap_or_default() | |
| 19 | +} | |
| 20 | + | |
| 21 | +fn norm(deg: f64) -> f64 { | |
| 22 | + deg.rem_euclid(360.0) | |
| 23 | +} | |
| 24 | + | |
| 25 | +fn sin(deg: f64) -> f64 { | |
| 26 | + deg.to_radians().sin() | |
| 27 | +} | |
| 28 | + | |
| 29 | +fn centuries(jd: f64) -> f64 { | |
| 30 | + (jd - 2451545.0) / 36525.0 | |
| 31 | +} | |
| 32 | + | |
| 33 | +/// The Sun's apparent ecliptic longitude, in degrees | |
| 34 | +pub fn sun_longitude(jd: f64) -> f64 { | |
| 35 | + let t = centuries(jd); | |
| 36 | + let l0 = 280.46646 + 36000.76983 * t + 0.0003032 * t * t; | |
| 37 | + let m = 357.52911 + 35999.05029 * t - 0.0001537 * t * t; | |
| 38 | + let c = (1.914602 - 0.004817 * t - 0.000014 * t * t) * sin(m) | |
| 39 | + + (0.019993 - 0.000101 * t) * sin(2.0 * m) | |
| 40 | + + 0.000289 * sin(3.0 * m); | |
| 41 | + let omega = 125.04 - 1934.136 * t; | |
| 42 | + norm(l0 + c - 0.00569 - 0.00478 * sin(omega)) | |
| 43 | +} | |
| 44 | + | |
| 45 | +/// The Moon's apparent ecliptic longitude, in degrees | |
| 46 | +pub fn moon_longitude(jd: f64) -> f64 { | |
| 47 | + let t = centuries(jd); | |
| 48 | + let t2 = t * t; | |
| 49 | + let t3 = t2 * t; | |
| 50 | + let t4 = t3 * t; | |
| 51 | + let lp = 218.3164477 + 481267.88123421 * t - 0.0015786 * t2 + t3 / 538841.0 - t4 / 65194000.0; | |
| 52 | + let d = 297.8501921 + 445267.1114034 * t - 0.0018819 * t2 + t3 / 545868.0 - t4 / 113065000.0; | |
| 53 | + let m = 357.5291092 + 35999.0502909 * t - 0.0001536 * t2 + t3 / 24490000.0; | |
| 54 | + let mp = 134.9633964 + 477198.8675055 * t + 0.0087414 * t2 + t3 / 69699.0 - t4 / 14712000.0; | |
| 55 | + let f = 93.2720950 + 483202.0175233 * t - 0.0036539 * t2 - t3 / 3526000.0 + t4 / 863310000.0; | |
| 56 | + let e = 1.0 - 0.002516 * t - 0.0000074 * t2; | |
| 57 | + | |
| 58 | + // The largest terms of Meeus' table 47.A: multiples of D, M, M', F and the coefficient in 1e-6 degrees | |
| 59 | + #[rustfmt::skip] | |
| 60 | + const TERMS: [(f64, f64, f64, f64, f64); 34] = [ | |
| 61 | + (0.0, 0.0, 1.0, 0.0, 6288774.0), (2.0, 0.0, -1.0, 0.0, 1274027.0), | |
| 62 | + (2.0, 0.0, 0.0, 0.0, 658314.0), (0.0, 0.0, 2.0, 0.0, 213618.0), | |
| 63 | + (0.0, 1.0, 0.0, 0.0, -185116.0), (0.0, 0.0, 0.0, 2.0, -114332.0), | |
| 64 | + (2.0, 0.0, -2.0, 0.0, 58793.0), (2.0, -1.0, -1.0, 0.0, 57066.0), | |
| 65 | + (2.0, 0.0, 1.0, 0.0, 53322.0), (2.0, -1.0, 0.0, 0.0, 45758.0), | |
| 66 | + (0.0, 1.0, -1.0, 0.0, -40923.0), (1.0, 0.0, 0.0, 0.0, -34720.0), | |
| 67 | + (0.0, 1.0, 1.0, 0.0, -30383.0), (2.0, 0.0, 0.0, -2.0, 15327.0), | |
| 68 | + (0.0, 0.0, 1.0, 2.0, -12528.0), (0.0, 0.0, 1.0, -2.0, 10980.0), | |
| 69 | + (4.0, 0.0, -1.0, 0.0, 10675.0), (0.0, 0.0, 3.0, 0.0, 10034.0), | |
| 70 | + (4.0, 0.0, -2.0, 0.0, 8548.0), (2.0, 1.0, -1.0, 0.0, -7888.0), | |
| 71 | + (2.0, 1.0, 0.0, 0.0, -6766.0), (1.0, 0.0, -1.0, 0.0, -5163.0), | |
| 72 | + (1.0, 1.0, 0.0, 0.0, 4987.0), (2.0, -1.0, 1.0, 0.0, 4036.0), | |
| 73 | + (2.0, 0.0, 2.0, 0.0, 3994.0), (4.0, 0.0, 0.0, 0.0, 3861.0), | |
| 74 | + (2.0, 0.0, -3.0, 0.0, 3665.0), (0.0, 1.0, -2.0, 0.0, -2689.0), | |
| 75 | + (2.0, 0.0, -1.0, 2.0, -2602.0), (2.0, -1.0, -2.0, 0.0, 2390.0), | |
| 76 | + (1.0, 0.0, 1.0, 0.0, -2348.0), (2.0, -2.0, 0.0, 0.0, 2236.0), | |
| 77 | + (0.0, 1.0, 2.0, 0.0, -2120.0), (0.0, 2.0, 0.0, 0.0, -2069.0), | |
| 78 | + ]; | |
| 79 | + let mut sum = 0.0; | |
| 80 | + for (cd, cm, cmp, cf, coeff) in TERMS { | |
| 81 | + // Terms with the Sun's anomaly shrink as Earth's orbit becomes less eccentric | |
| 82 | + let ecc = match cm.abs() as i32 { | |
| 83 | + 1 => e, | |
| 84 | + 2 => e * e, | |
| 85 | + _ => 1.0, | |
| 86 | + }; | |
| 87 | + sum += coeff * ecc * sin(cd * d + cm * m + cmp * mp + cf * f); | |
| 88 | + } | |
| 89 | + let a1 = 119.75 + 131.849 * t; | |
| 90 | + let a2 = 53.09 + 479264.290 * t; | |
| 91 | + sum += 3958.0 * sin(a1) + 1962.0 * sin(lp - f) + 318.0 * sin(a2); | |
| 92 | + let omega = 125.04 - 1934.136 * t; | |
| 93 | + norm(lp + sum / 1e6 - 0.00478 * sin(omega)) | |
| 94 | +} | |
| 95 | + | |
| 96 | +/// How far the Moon is ahead of the Sun: 0 at new moon, 180 at full | |
| 97 | +pub fn elongation(jd: f64) -> f64 { | |
| 98 | + norm(moon_longitude(jd) - sun_longitude(jd)) | |
| 99 | +} | |
| 100 | + | |
| 101 | +/// The Lahiri ayanamsa: how far the sidereal zodiac lags the tropical one | |
| 102 | +fn ayanamsa(jd: f64) -> f64 { | |
| 103 | + 23.853 + (jd - 2451545.0) / 365.25 * 0.013969 | |
| 104 | +} | |
| 105 | + | |
| 106 | +pub fn sidereal_sun_longitude(jd: f64) -> f64 { | |
| 107 | + norm(sun_longitude(jd) - ayanamsa(jd)) | |
| 108 | +} | |
| 109 | + | |
| 110 | +/// Every time between `from` and `to` that `f` passes `target` degrees, `f` always increasing | |
| 111 | +fn crossings(from: f64, to: f64, target: f64, step: f64, f: impl Fn(f64) -> f64) -> Vec<f64> { | |
| 112 | + let off = |jd: f64| (f(jd) - target + 540.0).rem_euclid(360.0) - 180.0; | |
| 113 | + let mut out = vec![]; | |
| 114 | + let mut a = from; | |
| 115 | + while a < to { | |
| 116 | + let b = a + step; | |
| 117 | + if off(a) < 0.0 && off(b) >= 0.0 { | |
| 118 | + let (mut lo, mut hi) = (a, b); | |
| 119 | + for _ in 0..40 { | |
| 120 | + let mid = (lo + hi) / 2.0; | |
| 121 | + if off(mid) < 0.0 { | |
| 122 | + lo = mid | |
| 123 | + } else { | |
| 124 | + hi = mid | |
| 125 | + } | |
| 126 | + } | |
| 127 | + out.push((lo + hi) / 2.0); | |
| 128 | + } | |
| 129 | + a = b; | |
| 130 | + } | |
| 131 | + out | |
| 132 | +} | |
| 133 | + | |
| 134 | +/// When the Moon reaches a phase angle (0 new, 90 first quarter, 180 full, 270 last quarter) | |
| 135 | +pub fn moon_phases(from: f64, to: f64, angle: f64) -> Vec<f64> { | |
| 136 | + crossings(from, to, angle, 1.0, elongation) | |
| 137 | +} | |
| 138 | + | |
| 139 | +/// When the Sun enters a sidereal longitude, e.g. 270 for Makara (Capricorn) | |
| 140 | +pub fn sidereal_ingress(from: f64, to: f64, longitude: f64) -> Vec<f64> { | |
| 141 | + crossings(from, to, longitude, 5.0, sidereal_sun_longitude) | |
| 142 | +} | |
| 143 | + | |
| 144 | +/// When the Sun reaches a tropical longitude: 0 and 180 are the equinoxes | |
| 145 | +pub fn sun_ingress(from: f64, to: f64, longitude: f64) -> Vec<f64> { | |
| 146 | + crossings(from, to, longitude, 5.0, sun_longitude) | |
| 147 | +} | |
| 148 | + | |
| 149 | +/// Julian day of midnight UTC starting a date, shifted by hours | |
| 150 | +pub fn jd_of(date: NaiveDate, hours: f64) -> f64 { | |
| 151 | + jd(date.and_hms_opt(0, 0, 0).unwrap_or_default()) + hours / 24.0 | |
| 152 | +} | |
| 153 | + | |
| 154 | +/// Sunrise on a date at a latitude and longitude (east positive), as a Julian day. | |
| 155 | +/// The NOAA approximation, good to a minute or two away from the poles. | |
| 156 | +pub fn sunrise(date: NaiveDate, lat: f64, lon: f64) -> f64 { | |
| 157 | + let noon = jd_of(date, 12.0 - lon / 15.0); | |
| 158 | + let t = centuries(noon); | |
| 159 | + let l0 = norm(280.46646 + 36000.76983 * t); | |
| 160 | + let m = 357.52911 + 35999.05029 * t; | |
| 161 | + let e = 0.016708634 - 0.000042037 * t; | |
| 162 | + let eps = 23.439291 - 0.0130042 * t; | |
| 163 | + let decl = (sin(eps) * sin(sun_longitude(noon))).asin(); | |
| 164 | + let y = (eps / 2.0).to_radians().tan().powi(2); | |
| 165 | + let (l0r, mr) = (l0.to_radians(), m.to_radians()); | |
| 166 | + // Equation of time, in minutes | |
| 167 | + let eot = 4.0 | |
| 168 | + * (y * (2.0 * l0r).sin() - 2.0 * e * mr.sin() + 4.0 * e * y * mr.sin() * (2.0 * l0r).cos() | |
| 169 | + - 0.5 * y * y * (4.0 * l0r).sin() | |
| 170 | + - 1.25 * e * e * (2.0 * mr).sin()) | |
| 171 | + .to_degrees(); | |
| 172 | + let phi = lat.to_radians(); | |
| 173 | + let cos_h = | |
| 174 | + ((-0.833f64).to_radians().sin() - phi.sin() * decl.sin()) / (phi.cos() * decl.cos()); | |
| 175 | + let h = cos_h.clamp(-1.0, 1.0).acos().to_degrees(); | |
| 176 | + let minutes_utc = 720.0 - 4.0 * lon - eot - 4.0 * h; | |
| 177 | + jd_of(date, minutes_utc / 60.0) | |
| 178 | +} | |
| 179 | + | |
| 180 | +/// A UTC time as a date and time in a fixed offset from UTC | |
| 181 | +pub fn local(jd: f64, offset_hours: f64) -> NaiveDateTime { | |
| 182 | + utc(jd).naive_utc() + Duration::seconds((offset_hours * 3600.0) as i64) | |
| 183 | +} | |
| 184 | + | |
| 185 | +#[cfg(test)] | |
| 186 | +mod tests { | |
| 187 | + use super::*; | |
| 188 | + | |
| 189 | + fn at(y: i32, m: u32, d: u32, h: u32, min: u32) -> f64 { | |
| 190 | + jd(NaiveDate::from_ymd_opt(y, m, d) | |
| 191 | + .unwrap() | |
| 192 | + .and_hms_opt(h, min, 0) | |
| 193 | + .unwrap()) | |
| 194 | + } | |
| 195 | + | |
| 196 | + #[test] | |
| 197 | + fn phases_match_eclipses() { | |
| 198 | + // The 2024-04-08 total solar eclipse new moon was at 18:21 UTC | |
| 199 | + let nm = moon_phases(at(2024, 4, 1, 0, 0), at(2024, 4, 15, 0, 0), 0.0); | |
| 200 | + assert_eq!(nm.len(), 1); | |
| 201 | + assert!( | |
| 202 | + (nm[0] - at(2024, 4, 8, 18, 21)).abs() < 10.0 / 1440.0, | |
| 203 | + "{}", | |
| 204 | + utc(nm[0]) | |
| 205 | + ); | |
| 206 | + // The 2024-09-18 lunar eclipse full moon was at 02:34 UTC | |
| 207 | + let fm = moon_phases(at(2024, 9, 10, 0, 0), at(2024, 9, 25, 0, 0), 180.0); | |
| 208 | + assert!( | |
| 209 | + (fm[0] - at(2024, 9, 18, 2, 34)).abs() < 10.0 / 1440.0, | |
| 210 | + "{}", | |
| 211 | + utc(fm[0]) | |
| 212 | + ); | |
| 213 | + } | |
| 214 | + | |
| 215 | + #[test] | |
| 216 | + fn sunrise_in_delhi() { | |
| 217 | + // 2026-06-21 05:24 and 2026-12-21 07:10 IST | |
| 218 | + let day = |m, d| NaiveDate::from_ymd_opt(2026, m, d).unwrap(); | |
| 219 | + let near = |jd: f64, d: NaiveDate, h: u32, m: u32| { | |
| 220 | + let want = super::jd(d.and_hms_opt(h, m, 0).unwrap()) - 5.5 / 24.0; | |
| 221 | + (jd - want).abs() < 2.0 / 1440.0 | |
| 222 | + }; | |
| 223 | + assert!(near(sunrise(day(6, 21), 28.61, 77.21), day(6, 21), 5, 24)); | |
| 224 | + assert!(near(sunrise(day(12, 21), 28.61, 77.21), day(12, 21), 7, 10)); | |
| 225 | + } | |
| 226 | +} |
modified
src/data.rs
+261
−3
@@ -3,17 +3,22 @@ use crate::do_tui::{Editable, Editablez};
| 3 | 3 | use crate::entities::ppl::Model; |
| 4 | 4 | use crate::entities::prelude::Tier; |
| 5 | 5 | use crate::entities::prelude::Traits; |
| 6 | −use crate::entities::prelude::{Contact, Ppl, Relation, SigDate, TierDefaults, TraitDefaults}; | |
| 6 | +use crate::entities::prelude::{ | |
| 7 | + Calendar, Contact, Ppl, Relation, SigDate, TierDefaults, TraitDefaults, | |
| 8 | +}; | |
| 7 | 9 | use crate::entities::{ |
| 8 | − contact, ppl, relation, sig_date, tier, tier_defaults, trait_defaults, traits, | |
| 10 | + calendar, contact, ppl, relation, sig_date, tier, tier_defaults, trait_defaults, traits, | |
| 9 | 11 | }; |
| 12 | +use crate::holidays::{self, CalendarDef, Group}; | |
| 10 | 13 | use crate::PplError; |
| 11 | 14 | use chrono::{Local, NaiveDate}; |
| 12 | 15 | use color_eyre::owo_colors::OwoColorize; |
| 13 | 16 | use log::warn; |
| 14 | 17 | use ratatui::style::palette::tailwind::PURPLE; |
| 15 | 18 | use sea_orm::ActiveValue::Set; |
| 16 | −use sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr, EntityTrait, NotSet}; | |
| 19 | +use sea_orm::{ | |
| 20 | + ActiveModelTrait, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, NotSet, QueryFilter, | |
| 21 | +}; | |
| 17 | 22 | use std::fmt; |
| 18 | 23 | use std::io; |
| 19 | 24 | use std::str::FromStr; |
@@ -775,6 +780,210 @@ pub async fn delete_editable(db: &DatabaseConnection, editable: &Editable) -> Re
| 775 | 780 | Ok(()) |
| 776 | 781 | } |
| 777 | 782 | |
| 783 | +pub struct CalendarOps {} | |
| 784 | + | |
| 785 | +impl CalendarOps { | |
| 786 | + /// Keys of the built-in calendars that are switched on | |
| 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) | |
| 791 | + .await? | |
| 792 | + .into_iter() | |
| 793 | + .filter_map(|c| holidays::find(&c.key).map(|d| d.key.to_string())) | |
| 794 | + .collect()) | |
| 795 | + } | |
| 796 | + | |
| 797 | + /// Switch a calendar on or off. A state switches its country on with it, and a | |
| 798 | + /// country switched off takes its states with it. Returns every calendar changed. | |
| 799 | + pub async fn set( | |
| 800 | + db: &DatabaseConnection, | |
| 801 | + key: &str, | |
| 802 | + on: bool, | |
| 803 | + ) -> Result<Vec<&'static CalendarDef>, PplError> { | |
| 804 | + let Some(def) = holidays::find(key) else { | |
| 805 | + return Err(invalid(format!( | |
| 806 | + "no calendar '{}', see `ppl calendars` for the list", | |
| 807 | + key | |
| 808 | + ))); | |
| 809 | + }; | |
| 810 | + let mut affected = vec![def]; | |
| 811 | + match on { | |
| 812 | + true => affected.extend(def.parent.and_then(holidays::find)), | |
| 813 | + false => affected.extend(holidays::regions(def.key)), | |
| 814 | + } | |
| 815 | + let enabled = Self::enabled(db).await?; | |
| 816 | + let mut changed = vec![]; | |
| 817 | + for d in affected { | |
| 818 | + if enabled.iter().any(|k| k == d.key) == on { | |
| 819 | + continue; | |
| 820 | + } | |
| 821 | + Self::upsert(db, d.key, |row| row.enabled = Set(on)).await?; | |
| 822 | + changed.push(d); | |
| 823 | + } | |
| 824 | + Ok(changed) | |
| 825 | + } | |
| 826 | + | |
| 827 | + /// Every row: calendars that have been switched on or given reminder settings, and group defaults | |
| 828 | + pub async fn rows(db: &DatabaseConnection) -> Result<Vec<calendar::Model>, DbErr> { | |
| 829 | + Calendar::find().all(db).await | |
| 830 | + } | |
| 831 | + | |
| 832 | + /// Set the motd window and mode of a calendar or group; None goes back to the default | |
| 833 | + pub async fn set_reminder( | |
| 834 | + db: &DatabaseConnection, | |
| 835 | + target: ReminderTarget, | |
| 836 | + delta: Option<u32>, | |
| 837 | + remind: Option<MotdRemindEnum>, | |
| 838 | + ) -> Result<(), PplError> { | |
| 839 | + Self::upsert(db, &target.row_key(), |row| { | |
| 840 | + row.sig_date_delta = Set(delta); | |
| 841 | + row.sig_remind_enum = Set(remind.map(|r| r.to_string())); | |
| 842 | + }) | |
| 843 | + .await | |
| 844 | + } | |
| 845 | + | |
| 846 | + /// Note that the motd just showed this calendar, for the daily and hourly modes | |
| 847 | + pub async fn mark_reminded( | |
| 848 | + db: &DatabaseConnection, | |
| 849 | + row: &calendar::Model, | |
| 850 | + day: &str, | |
| 851 | + hour: &str, | |
| 852 | + ) -> Result<(), PplError> { | |
| 853 | + let mut meta = match &row.meta { | |
| 854 | + Some(serde_json::Value::Object(m)) => m.clone(), | |
| 855 | + _ => serde_json::Map::new(), | |
| 856 | + }; | |
| 857 | + meta.insert("last_reminded".to_string(), day.into()); | |
| 858 | + meta.insert("last_reminded_hour".to_string(), hour.into()); | |
| 859 | + Self::upsert(db, &row.key, |r| { | |
| 860 | + r.meta = Set(Some(serde_json::Value::Object(meta))) | |
| 861 | + }) | |
| 862 | + .await | |
| 863 | + } | |
| 864 | + | |
| 865 | + /// Change a row, creating it (switched off) if it isn't there yet | |
| 866 | + async fn upsert( | |
| 867 | + db: &DatabaseConnection, | |
| 868 | + key: &str, | |
| 869 | + change: impl FnOnce(&mut calendar::ActiveModel), | |
| 870 | + ) -> Result<(), PplError> { | |
| 871 | + let today = Local::now().date_naive(); | |
| 872 | + let existing = Calendar::find() | |
| 873 | + .filter(calendar::Column::Key.eq(key)) | |
| 874 | + .one(db) | |
| 875 | + .await?; | |
| 876 | + let exists = existing.is_some(); | |
| 877 | + let mut row = match existing { | |
| 878 | + Some(row) => row.into(), | |
| 879 | + None => calendar::ActiveModel { | |
| 880 | + id: NotSet, | |
| 881 | + key: Set(key.to_string()), | |
| 882 | + enabled: Set(false), | |
| 883 | + sig_date_delta: Set(None), | |
| 884 | + sig_remind_enum: Set(None), | |
| 885 | + meta: Set(None), | |
| 886 | + date_ins: Set(today), | |
| 887 | + date_up: Set(today), | |
| 888 | + }, | |
| 889 | + }; | |
| 890 | + change(&mut row); | |
| 891 | + row.date_up = Set(today); | |
| 892 | + match exists { | |
| 893 | + true => row.update(db).await?, | |
| 894 | + false => row.insert(db).await?, | |
| 895 | + }; | |
| 896 | + Ok(()) | |
| 897 | + } | |
| 898 | +} | |
| 899 | + | |
| 900 | +/// What a motd reminder setting is attached to: one calendar, or a group's default | |
| 901 | +#[derive(Clone, Copy, Debug)] | |
| 902 | +pub enum ReminderTarget { | |
| 903 | + Calendar(&'static CalendarDef), | |
| 904 | + Group(&'static Group), | |
| 905 | +} | |
| 906 | + | |
| 907 | +impl ReminderTarget { | |
| 908 | + /// A calendar key, or a group name: countries, religious or other | |
| 909 | + pub fn parse(s: &str) -> Result<Self, PplError> { | |
| 910 | + if let Some(g) = holidays::GROUPS | |
| 911 | + .iter() | |
| 912 | + .find(|g| g.key.eq_ignore_ascii_case(s)) | |
| 913 | + { | |
| 914 | + return Ok(ReminderTarget::Group(g)); | |
| 915 | + } | |
| 916 | + holidays::find(s) | |
| 917 | + .map(ReminderTarget::Calendar) | |
| 918 | + .ok_or_else(|| { | |
| 919 | + let groups = holidays::GROUPS.iter().map(|g| g.key).collect::<Vec<_>>(); | |
| 920 | + invalid(format!( | |
| 921 | + "no calendar or group '{}' (groups: {}; calendars: `ppl calendars list --all`)", | |
| 922 | + s, | |
| 923 | + groups.join(", ") | |
| 924 | + )) | |
| 925 | + }) | |
| 926 | + } | |
| 927 | + | |
| 928 | + fn row_key(&self) -> String { | |
| 929 | + match self { | |
| 930 | + ReminderTarget::Calendar(c) => c.key.to_string(), | |
| 931 | + ReminderTarget::Group(g) => format!("group:{}", g.key), | |
| 932 | + } | |
| 933 | + } | |
| 934 | + | |
| 935 | + pub fn label(&self) -> &'static str { | |
| 936 | + match self { | |
| 937 | + ReminderTarget::Calendar(c) => c.name, | |
| 938 | + ReminderTarget::Group(g) => g.name, | |
| 939 | + } | |
| 940 | + } | |
| 941 | + | |
| 942 | + /// The setting itself, and what it falls back to: a state uses its country's, then a | |
| 943 | + /// calendar uses its group's, then holidays show on the day, once a day | |
| 944 | + pub fn reminder(&self, rows: &[calendar::Model]) -> CalendarReminder { | |
| 945 | + let row = |key: &str| rows.iter().find(|r| r.key == key); | |
| 946 | + let parse = |r: &calendar::Model| { | |
| 947 | + r.sig_remind_enum | |
| 948 | + .as_deref() | |
| 949 | + .and_then(|m| m.parse::<MotdRemindEnum>().ok()) | |
| 950 | + }; | |
| 951 | + let own = row(&self.row_key()); | |
| 952 | + let mut fallbacks = vec![]; | |
| 953 | + if let ReminderTarget::Calendar(c) = self { | |
| 954 | + fallbacks.extend(c.parent.and_then(row)); | |
| 955 | + fallbacks.extend(row(&format!("group:{}", holidays::group_of(c).key))); | |
| 956 | + } | |
| 957 | + CalendarReminder { | |
| 958 | + delta: own.and_then(|r| r.sig_date_delta), | |
| 959 | + remind: own.and_then(parse), | |
| 960 | + default_delta: fallbacks.iter().find_map(|r| r.sig_date_delta).or(Some(0)), | |
| 961 | + default_remind: fallbacks | |
| 962 | + .iter() | |
| 963 | + .find_map(|r| parse(r)) | |
| 964 | + .or(Some(MotdRemindEnum::OnlyOnce)), | |
| 965 | + } | |
| 966 | + } | |
| 967 | +} | |
| 968 | + | |
| 969 | +/// A calendar's or group's motd settings, with the defaults they fall back to | |
| 970 | +#[derive(Clone, Copy, Debug)] | |
| 971 | +pub struct CalendarReminder { | |
| 972 | + pub delta: Option<u32>, | |
| 973 | + pub remind: Option<MotdRemindEnum>, | |
| 974 | + pub default_delta: Option<u32>, | |
| 975 | + pub default_remind: Option<MotdRemindEnum>, | |
| 976 | +} | |
| 977 | + | |
| 978 | +impl CalendarReminder { | |
| 979 | + pub fn window(&self) -> u32 { | |
| 980 | + self.delta.or(self.default_delta).unwrap_or(0) | |
| 981 | + } | |
| 982 | + pub fn mode(&self) -> MotdRemindEnum { | |
| 983 | + self.remind.or(self.default_remind).unwrap_or_default() | |
| 984 | + } | |
| 985 | +} | |
| 986 | + | |
| 778 | 987 | pub struct TierDefaultOps {} |
| 779 | 988 | |
| 780 | 989 | impl TierDefaultOps { |
@@ -965,3 +1174,52 @@ impl TraitDefaultOps {
| 965 | 1174 | Ok(key.to_string()) |
| 966 | 1175 | } |
| 967 | 1176 | } |
| 1177 | + | |
| 1178 | +#[cfg(test)] | |
| 1179 | +mod tests { | |
| 1180 | + use super::*; | |
| 1181 | + | |
| 1182 | + fn row(key: &str, delta: Option<u32>, remind: Option<&str>) -> calendar::Model { | |
| 1183 | + let today = NaiveDate::from_ymd_opt(2026, 10, 19).unwrap(); | |
| 1184 | + calendar::Model { | |
| 1185 | + id: 0, | |
| 1186 | + key: key.to_string(), | |
| 1187 | + enabled: true, | |
| 1188 | + sig_date_delta: delta, | |
| 1189 | + sig_remind_enum: remind.map(|r| r.to_string()), | |
| 1190 | + meta: None, | |
| 1191 | + date_ins: today, | |
| 1192 | + date_up: today, | |
| 1193 | + } | |
| 1194 | + } | |
| 1195 | + | |
| 1196 | + #[test] | |
| 1197 | + fn calendar_reminders_fall_back() { | |
| 1198 | + let rows = vec![ | |
| 1199 | + row("group:religious", Some(7), None), | |
| 1200 | + row("us", Some(3), None), | |
| 1201 | + row("us-ny", None, Some("OnceHourly")), | |
| 1202 | + ]; | |
| 1203 | + let get = |t: &str| ReminderTarget::parse(t).unwrap().reminder(&rows); | |
| 1204 | + // A state takes its country's window, and keeps its own mode | |
| 1205 | + let ny = get("us-ny"); | |
| 1206 | + assert_eq!((ny.window(), ny.mode()), (3, MotdRemindEnum::OnceHourly)); | |
| 1207 | + assert_eq!(ny.delta, None); | |
| 1208 | + let jewish = get("jewish"); | |
| 1209 | + assert_eq!( | |
| 1210 | + (jewish.window(), jewish.mode()), | |
| 1211 | + (7, MotdRemindEnum::OnlyOnce) | |
| 1212 | + ); | |
| 1213 | + // With nothing set: on the day, once a day | |
| 1214 | + let lunar = get("lunar"); | |
| 1215 | + assert_eq!( | |
| 1216 | + (lunar.window(), lunar.mode()), | |
| 1217 | + (0, MotdRemindEnum::OnlyOnce) | |
| 1218 | + ); | |
| 1219 | + assert!(ReminderTarget::parse("nope").is_err()); | |
| 1220 | + assert!(matches!( | |
| 1221 | + ReminderTarget::parse("Religious"), | |
| 1222 | + Ok(ReminderTarget::Group(_)) | |
| 1223 | + )); | |
| 1224 | + } | |
| 1225 | +} |
added
src/do_calendars.rs
+193
−0
Click to load diff…
modified
src/do_tui.rs
+243
−28
Click to load diff…
added
src/entities/calendar.rs
+22
−0
Click to load diff…
modified
src/entities/mod.rs
+1
−0
Click to load diff…
modified
src/entities/prelude.rs
+1
−0
Click to load diff…
added
src/holiday_info.tsv
+217
−0
Click to load diff…
added
src/holidays.rs
+1775
−0
Click to load diff…
modified
src/main.rs
+129
−18
Click to load diff…
added
src/migrator/m20261019_000013_create_calendar.rs
+62
−0
Click to load diff…
added
src/migrator/m20261019_000014_add_reminders_to_calendar.rs
+71
−0
Click to load diff…
modified
src/migrator/mod.rs
+4
−0
Click to load diff…
modified
src/tui_add.rs
+1
−1
Click to load diff…
added
src/tui_calendars.rs
+230
−0
Click to load diff…
Parents: 2803012