diff options
| author | Jesse Morgan <jesse@jesterpm.net> | 2026-01-02 16:32:24 -0800 |
|---|---|---|
| committer | Jesse Morgan <jesse@jesterpm.net> | 2026-01-02 16:49:23 -0800 |
| commit | 8d1c655b32c365a7ec18cc0d4258c9d0a1d92d38 (patch) | |
| tree | d1edd82ef6631269834f168595e0f11bc74c139b | |
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.lock | 1044 | ||||
| -rw-r--r-- | Cargo.toml | 17 | ||||
| -rw-r--r-- | README.md | 141 | ||||
| -rw-r--r-- | src/main.rs | 406 |
5 files changed, 1610 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..890bfb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +*.sw[lmnop] diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1abdb9a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1044 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bumpalo" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "clap" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.0", +] + +[[package]] +name = "clap_derive" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c376d08ea6aa96aafe61237c7200d1241cb177b7d3a542d791f2d118e9cbb955" +dependencies = [ + "darling_core 0.20.6", + "darling_macro 0.20.6", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33043dcd19068b8192064c704b3f83eb464f91f1ff527b44a4e2b08d9cdb8855" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.49", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be" +dependencies = [ + "darling_core 0.20.6", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "earcutr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01" +dependencies = [ + "itertools", + "num-traits", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fit2mf2" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "fitparser", + "geo", + "geo-uri", + "serde", + "serde_json", + "serde_with", +] + +[[package]] +name = "fitparser" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01b8defcd147fd7fa4536295e4c47ef7a592c128627fd44b3f79f0b9defda35" +dependencies = [ + "chrono", + "nom", + "serde", +] + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "geo" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4841b40fdbccd4b7042bd6195e4de91da54af34c50632e371bcbfcdfb558b873" +dependencies = [ + "earcutr", + "float_next_after", + "geo-types", + "geographiclib-rs", + "log", + "num-traits", + "robust", + "rstar", + "spade", +] + +[[package]] +name = "geo-types" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567495020b114f1ce9bed679b29975aa0bfae06ac22beacd5cfde5dabe7b05d6" +dependencies = [ + "approx", + "num-traits", + "rstar", + "serde", +] + +[[package]] +name = "geo-uri" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6b8812a079cb4a3b5b366c2d05db8e231e2f231c443acb3532daad996d89f71" +dependencies = [ + "derive_builder", + "serde", + "thiserror", +] + +[[package]] +name = "geographiclib-rs" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e5ed84f8089c70234b0a8e0aedb6dc733671612ddc0d37c6066052f9781960" +dependencies = [ + "libm", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "robust" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30" + +[[package]] +name = "rstar" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6" +dependencies = [ + "heapless", + "num-traits", + "smallvec", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.3", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +dependencies = [ + "darling 0.20.6", + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "spade" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61addf9117b11d1f5b4bf6fe94242ba25f59d2d4b2080544b771bd647024fd00" +dependencies = [ + "hashbrown 0.14.3", + "num-traits", + "robust", + "smallvec", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.49", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5c862bf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fit2mf2" +version = "0.1.0" +description = "Convert a Garmin FIT file into a Microformat entry" +repository = "https://git.jesterpm.net/pub/jesterpm/fit2mf.git" +license = "MIT" +edition = "2021" + +[dependencies] +clap = { version = "4.4.14", features = ["derive"] } +chrono = "0.4" +fitparser = "0.6" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_with = "3.6" +geo-uri = { version = "0.2.1", features = ["serde"] } +geo = "0.27.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..365f9c8 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +fit2mf2 +======= + +A utility to convert a Garmin FIT data file into a microformat entry. + + +Usage +----- + + Usage: fit2mf2 [OPTIONS] [FILE] + + Arguments: + [FILE] FIT file to convert. If no file is given, reads from stdin. + + Options: + -t, --category <CATEGORY> Category to add to the entry. + May be given more than once. + + --hide <lat,long,radius> Location to mask in output. Radius in + meters. May be given more than once. + + -h, --help + + +Sample output +------------- + + { + "type": [ + "h-entry" + ] + "properties": { + "activity": [ + { + "sport": "running", + "start": "2021-06-19T14:51:50Z", + "start_position": "geo:47.6264,-122.3371", + "end_position": "geo:47.6264,-122.3371", + "max_heart_rate": { + "num": 195.0, + "unit": "bpm" + }, + "max_speed": { + "num": 3.76, + "unit": "m/s" + }, + "timer": { + "num": 3742.613, + "unit": "s" + }, + "ascent": { + "num": 115.0, + "unit": "m" + }, + "avg_heart_rate": { + "num": 169.0, + "unit": "bpm" + }, + "avg_speed": { + "num": 2.7, + "unit": "m/s" + }, + "calories": { + "num": 691.0, + "unit": "kcal" + }, + "descent": { + "num": 171.0, + "unit": "m" + }, + "distance": { + "num": 10104.88, + "unit": "m" + }, + "duration": { + "num": 3742.613, + "unit": "s" + }, + "laps": [ + { + "sport": "running", + "start": "2021-06-19T14:51:50Z", + "start_position": "geo:47.6264,-122.3371", + "end_position": "geo:47.6336,-122.3401", + "ascent": { + "num": 7.0, + "unit": "m" + }, + "avg_heart_rate": { + "num": 144.0, + "unit": "bpm" + }, + "avg_speed": { + "num": 2.876, + "unit": "m/s" + }, + "calories": { + "num": 53.0, + "unit": "kcal" + }, + "descent": { + "num": 2.0, + "unit": "m" + }, + "distance": { + "num": 1000.0, + "unit": "m" + }, + "duration": { + "num": 347.712, + "unit": "s" + }, + "max_heart_rate": { + "num": 177.0, + "unit": "bpm" + }, + "max_speed": { + "num": 3.602, + "unit": "m/s" + }, + "timer": { + "num": 347.712, + "unit": "s" + } + }, + ... + ] + } + ], + "location": [ + "geo:47.6264,-122.3371" + ], + "published": "2021-06-19T14:51:50Z" + }, + } + +Contributing +------------ + +Send questions, bug reports, and patches to jesse@jesterpm.net. + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d2c8f0d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,406 @@ +use chrono::{DateTime, Utc}; +use clap::Parser; +use fitparser::profile::MesgNum; +use fitparser::{self, FitDataField, FitDataRecord}; +use geo::algorithm::geodesic_distance::GeodesicDistance; +use geo::Point; +use geo_uri::GeoUri; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::fs::File; +use std::io::{prelude::*, stdin, stdout}; +use std::process; +use std::str::FromStr; + +#[derive(Parser, Debug)] +struct Args { + #[arg(short = 't', long = "category")] + category: Vec<String>, + + #[arg(long = "hide")] + hidden_location: Vec<ExclusionPoint>, + + file: Option<String>, +} + +const SEMICIRCLE: f64 = 11930465.0; + +#[derive(Debug, Copy, Clone)] +struct ExclusionPoint { + center: Point, + radius: f64, +} + +impl ExclusionPoint { + pub fn contains(&self, pos: &Point) -> bool { + let distance = self.center.geodesic_distance(pos); + distance <= self.radius + } +} + +impl FromStr for ExclusionPoint { + type Err = Box<dyn std::error::Error>; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut it = s.split(','); + let latitude = it.next().unwrap().parse()?; + let longitude = it.next().unwrap().parse()?; + let radius = it.next().unwrap().parse()?; + Ok(ExclusionPoint { + center: Point::new(longitude, latitude), + radius, + }) + } +} + +impl From<String> for ExclusionPoint { + fn from(s: String) -> Self { + Self::from_str(&s).unwrap() + } +} + +#[derive(Serialize, Deserialize, Clone)] +struct Measure { + num: f64, + unit: String, +} + +fn as_measure(value: &FitDataField) -> Option<Measure> { + let num = match *value.value() { + fitparser::Value::Byte(v) => v as f64, + fitparser::Value::SInt8(v) => v as f64, + fitparser::Value::UInt8(v) => v as f64, + fitparser::Value::SInt16(v) => v as f64, + fitparser::Value::UInt16(v) => v as f64, + fitparser::Value::SInt32(v) => v as f64, + fitparser::Value::UInt32(v) => v as f64, + fitparser::Value::Float32(v) => v as f64, + fitparser::Value::Float64(v) => v, + fitparser::Value::UInt8z(v) => { + if v == 0 { + return None; + } else { + v as f64 + } + } + fitparser::Value::UInt16z(v) => { + if v == 0 { + return None; + } else { + v as f64 + } + } + fitparser::Value::UInt32z(v) => { + if v == 0 { + return None; + } else { + v as f64 + } + } + fitparser::Value::SInt64(v) => v as f64, + fitparser::Value::UInt64(v) => v as f64, + fitparser::Value::UInt64z(v) => { + if v == 0 { + return None; + } else { + v as f64 + } + } + fitparser::Value::Timestamp(_) => return None, + fitparser::Value::Enum(_) => return None, + fitparser::Value::String(_) => return None, + fitparser::Value::Array(_) => return None, + }; + let unit = value.units().to_string(); + Some(Measure { num, unit }) +} + +#[serde_with::skip_serializing_none] +#[derive(Default, Serialize, Deserialize, Clone)] +struct Session { + sport: Option<String>, + start: Option<DateTime<Utc>>, + start_position: Option<GeoUri>, + end_position: Option<GeoUri>, + duration: Option<Measure>, + timer: Option<Measure>, + avg_heart_rate: Option<Measure>, + max_heart_rate: Option<Measure>, + min_temperature: Option<Measure>, + avg_temperature: Option<Measure>, + max_temperature: Option<Measure>, + avg_speed: Option<Measure>, + max_speed: Option<Measure>, + ascent: Option<Measure>, + descent: Option<Measure>, + calories: Option<Measure>, + distance: Option<Measure>, + #[serde(skip_serializing_if = "Vec::is_empty")] + laps: Vec<Session>, +} + +#[derive(Default, Serialize, Deserialize)] +struct LapSlice { + index: u64, + count: u64, +} + +#[derive(Default)] +struct PositionBuilder { + latitude: Option<f64>, + longitude: Option<f64>, +} + +impl PositionBuilder { + pub fn latitude(&mut self, field: &FitDataField) { + if let Some(value) = as_measure(field) { + self.latitude = Some(value.num / SEMICIRCLE); + } else { + self.latitude = None + } + } + + pub fn longitude(&mut self, field: &FitDataField) { + if let Some(value) = as_measure(field) { + self.longitude = Some(value.num / SEMICIRCLE); + } else { + self.longitude = None + } + } + + pub fn as_point(&self) -> Option<Point> { + if let (Some(lat), Some(lon)) = (self.latitude, self.longitude) { + Some(Point::new(lon, lat)) + } else { + None + } + } +} + +impl From<PositionBuilder> for Option<GeoUri> { + fn from(val: PositionBuilder) -> Self { + if let Some(lat) = val.latitude { + if let Some(long) = val.longitude { + return GeoUri::builder() + .latitude(round_geo(lat)) + .longitude(round_geo(long)) + .build() + .ok(); + } + } + None + } +} + +/// Truncate GPS noise +fn round_geo(x: f64) -> f64 { + (x * 100000.0).round() / 100000.0 +} + +fn parse_session(args: &Args, record: &FitDataRecord) -> (Session, LapSlice) { + let mut session = Session::default(); + let mut laps = LapSlice::default(); + let mut start_position = PositionBuilder::default(); + let mut end_position = PositionBuilder::default(); + for field in record.fields() { + match field.name() { + "sport" => session.sport = Some(field.value().to_string()), + "start_time" => session.start = as_datetime(field), + "total_elapsed_time" => session.duration = as_measure(field), + "total_timer_time" => session.timer = as_measure(field), + "avg_heart_rate" => session.avg_heart_rate = as_measure(field), + "max_heart_rate" => session.max_heart_rate = as_measure(field), + "min_temperature" => session.min_temperature = as_measure(field), + "avg_temperature" => session.avg_temperature = as_measure(field), + "max_temperature" => session.max_temperature = as_measure(field), + "enhanced_avg_speed" => session.avg_speed = as_measure(field), + "enhanced_max_speed" => session.max_speed = as_measure(field), + "total_ascent" => session.ascent = as_measure(field), + "total_descent" => session.descent = as_measure(field), + "total_calories" => session.calories = as_measure(field), + "total_distance" => session.distance = as_measure(field), + "first_lap_index" => laps.index = as_u64(field).unwrap_or(0), + "num_laps" => laps.count = as_u64(field).unwrap_or(0), + "start_position_lat" => start_position.latitude(field), + "start_position_long" => start_position.longitude(field), + "end_position_lat" => end_position.latitude(field), + "end_position_long" => end_position.longitude(field), + _ => (), + } + } + + // Just say no to hidden locations + if let Some(p) = start_position.as_point() { + if !args.hidden_location.iter().any(|nope| nope.contains(&p)) { + session.start_position = start_position.into(); + } + } + if let Some(p) = end_position.as_point() { + if !args.hidden_location.iter().any(|nope| nope.contains(&p)) { + session.end_position = end_position.into(); + } + } + (session, laps) +} + +fn as_datetime(field: &FitDataField) -> Option<DateTime<Utc>> { + if let fitparser::Value::Timestamp(ts) = field.value() { + Some(ts.to_utc()) + } else { + None + } +} + +fn as_u64(field: &FitDataField) -> Option<u64> { + let value = match *field.value() { + fitparser::Value::Byte(v) => v as u64, + fitparser::Value::UInt8(v) => v as u64, + fitparser::Value::UInt16(v) => v as u64, + fitparser::Value::UInt32(v) => v as u64, + fitparser::Value::UInt64(v) => v, + fitparser::Value::SInt8(v) => { + if v >= 0 { + v as u64 + } else { + 0 + } + } + fitparser::Value::SInt16(v) => { + if v >= 0 { + v as u64 + } else { + 0 + } + } + fitparser::Value::SInt32(v) => { + if v >= 0 { + v as u64 + } else { + 0 + } + } + fitparser::Value::SInt64(v) => { + if v >= 0 { + v as u64 + } else { + 0 + } + } + fitparser::Value::Float32(_) => return None, + fitparser::Value::Float64(_) => return None, + fitparser::Value::UInt8z(v) => { + if v == 0 { + return None; + } else { + v as u64 + } + } + fitparser::Value::UInt16z(v) => { + if v == 0 { + return None; + } else { + v as u64 + } + } + fitparser::Value::UInt32z(v) => { + if v == 0 { + return None; + } else { + v as u64 + } + } + fitparser::Value::UInt64z(v) => { + if v == 0 { + return None; + } else { + v + } + } + fitparser::Value::Timestamp(_) => return None, + fitparser::Value::Enum(_) => return None, + fitparser::Value::String(_) => return None, + fitparser::Value::Array(_) => return None, + }; + Some(value) +} + +fn run() -> Result<(), Box<dyn std::error::Error>> { + let args = Args::parse(); + let mut reader: Box<dyn Read> = match args.file { + Some(ref filename) => Box::new(File::open(filename)?), + None => Box::new(stdin()), + }; + + let mut sessions = Vec::new(); + let mut laps = Vec::new(); + + for msg in fitparser::from_reader(&mut reader)? { + match msg.kind() { + MesgNum::Session => { + sessions.push(parse_session(&args, &msg)); + } + MesgNum::Lap => { + laps.push(parse_session(&args, &msg)); + } + _ => (), + } + } + + let activity: Vec<Session> = sessions + .into_iter() + .map(|(mut s, l)| { + let start = l.index as usize; + let end = (l.index + l.count) as usize; + if end > start { + s.laps = laps[start..end].iter().map(|(s, _)| s.clone()).collect(); + s + } else { + s + } + }) + .collect(); + + let location = activity + .iter() + .flat_map(|s| s.start_position) + .map(|uri| uri.to_string()) + .take(1) + .collect(); + + let properties = EntryProperties { + published: activity.iter().flat_map(|s| s.start).min(), + category: args.category.clone(), + location, + activity, + }; + + let output = json!({ + "type": ["h-entry"], + "properties": properties + }); + + serde_json::to_writer(stdout(), &output)?; + + Ok(()) +} + +#[serde_with::skip_serializing_none] +#[derive(Default, Serialize, Deserialize, Clone)] +struct EntryProperties { + published: Option<DateTime<Utc>>, + #[serde(skip_serializing_if = "Vec::is_empty")] + category: Vec<String>, + #[serde(skip_serializing_if = "Vec::is_empty")] + location: Vec<String>, + activity: Vec<Session>, +} + +fn main() { + process::exit(match run() { + Ok(_) => 0, + Err(e) => { + eprintln!("Error: {e}"); + 1 + } + }) +} |
