From feefdb81b95fa3d4b707b9ec946f68795235da23 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 25 May 2026 19:53:50 +0800 Subject: [PATCH] Committing before copilot touches this --- .gitignore | 1 + Cargo.lock | 4507 +++++++++++++++++++++++++++ Cargo.toml | 36 + README.md | 414 +++ bread.zip | Bin 0 -> 53251 bytes breadman/Cargo.toml | 22 + breadman/src/editor.rs | 162 + breadman/src/main.rs | 906 ++++++ breadman/src/views/archive.rs | 109 + breadman/src/views/errors.rs | 60 + breadman/src/views/mod.rs | 4 + breadman/src/views/settings.rs | 271 ++ breadman/src/views/upcoming.rs | 86 + breadpad-shared/Cargo.toml | 29 + breadpad-shared/src/ai.rs | 118 + breadpad-shared/src/calendar.rs | 221 ++ breadpad-shared/src/classifier.rs | 284 ++ breadpad-shared/src/config.rs | 180 ++ breadpad-shared/src/lib.rs | 9 + breadpad-shared/src/parser.rs | 882 ++++++ breadpad-shared/src/scheduler.rs | 369 +++ breadpad-shared/src/store.rs | 204 ++ breadpad-shared/src/theme.rs | 447 +++ breadpad-shared/src/types.rs | 404 +++ breadpad-shared/tests/classifier.rs | 83 + breadpad-shared/tests/config.rs | 198 ++ breadpad-shared/tests/pipeline.rs | 236 ++ breadpad-shared/tests/store.rs | 382 +++ breadpad-test/Cargo.toml | 20 + breadpad-test/corpus.json | 506 +++ breadpad-test/src/main.rs | 474 +++ breadpad.example.toml | 14 + breadpad/Cargo.toml | 24 + breadpad/src/main.rs | 573 ++++ breadpadcli | 1 + svgs.txt | 102 + 36 files changed, 12338 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 bread.zip create mode 100644 breadman/Cargo.toml create mode 100644 breadman/src/editor.rs create mode 100644 breadman/src/main.rs create mode 100644 breadman/src/views/archive.rs create mode 100644 breadman/src/views/errors.rs create mode 100644 breadman/src/views/mod.rs create mode 100644 breadman/src/views/settings.rs create mode 100644 breadman/src/views/upcoming.rs create mode 100644 breadpad-shared/Cargo.toml create mode 100644 breadpad-shared/src/ai.rs create mode 100644 breadpad-shared/src/calendar.rs create mode 100644 breadpad-shared/src/classifier.rs create mode 100644 breadpad-shared/src/config.rs create mode 100644 breadpad-shared/src/lib.rs create mode 100644 breadpad-shared/src/parser.rs create mode 100644 breadpad-shared/src/scheduler.rs create mode 100644 breadpad-shared/src/store.rs create mode 100644 breadpad-shared/src/theme.rs create mode 100644 breadpad-shared/src/types.rs create mode 100644 breadpad-shared/tests/classifier.rs create mode 100644 breadpad-shared/tests/config.rs create mode 100644 breadpad-shared/tests/pipeline.rs create mode 100644 breadpad-shared/tests/store.rs create mode 100644 breadpad-test/Cargo.toml create mode 100644 breadpad-test/corpus.json create mode 100644 breadpad-test/src/main.rs create mode 100644 breadpad.example.toml create mode 100644 breadpad/Cargo.toml create mode 100644 breadpad/src/main.rs create mode 120000 breadpadcli create mode 100644 svgs.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..eaf5cc9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4507 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[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 = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "breadman" +version = "0.1.0" +dependencies = [ + "anyhow", + "breadpad-shared", + "chrono", + "dirs 5.0.1", + "gtk4", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "breadpad" +version = "0.1.0" +dependencies = [ + "anyhow", + "breadpad-shared", + "chrono", + "dirs 5.0.1", + "gtk4", + "gtk4-layer-shell", + "hyprland", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "breadpad-shared" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "dirs 5.0.1", + "ical", + "ndarray 0.16.1", + "ort", + "regex", + "reqwest", + "rrule", + "serde", + "serde_json", + "tempfile", + "tokenizers", + "tokio", + "toml 0.8.23", + "tracing", + "ureq 2.12.1", + "uuid", + "zbus", +] + +[[package]] +name = "breadpad-test" +version = "0.1.0" +dependencies = [ + "anyhow", + "breadpad-shared", + "chrono", + "clap", + "colored", + "comfy-table", + "serde", + "serde_json", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cairo-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "comfy-table" +version = "7.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "document-features", + "parking_lot", + "rustix", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dary_heap" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" +dependencies = [ + "serde", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "gl", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.61.2", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glib" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-layer-shell" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" +dependencies = [ + "bitflags", + "gdk4", + "glib", + "glib-sys", + "gtk4", + "gtk4-layer-shell-sys", + "libc", +] + +[[package]] +name = "gtk4-layer-shell-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" +dependencies = [ + "gdk4-sys", + "glib-sys", + "gtk4-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk4-macros" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hf-hub" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" +dependencies = [ + "dirs 6.0.0", + "http", + "indicatif", + "libc", + "log", + "rand 0.9.4", + "serde", + "serde_json", + "thiserror 2.0.18", + "ureq 2.12.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.7", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "hyprland" +version = "0.4.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62fc24052f578592af91e5c60da1893ba6aea266b6ab86ffb72a644cf213fea9" +dependencies = [ + "async-stream", + "derive_more", + "either", + "futures-lite", + "hyprland-macros", + "pastey", + "serde", + "serde_json", + "serde_repr", + "tokio", +] + +[[package]] +name = "hyprland-macros" +version = "0.4.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31157e6ccefbad4b0cd7e549db6696691a70c11b108f26bf6bf76eef26af8c10" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "ical" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rust2" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619" + +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "monostate" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[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 = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "ort" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" +dependencies = [ + "ndarray 0.17.2", + "ort-sys", + "smallvec", + "tracing", + "ureq 3.3.0", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" +dependencies = [ + "hmac-sha256", + "lzma-rust2", + "ureq 3.3.0", +] + +[[package]] +name = "pango" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.6", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.7", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rrule" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa903a94a0360e2b693d30028605c10105c5a830e6fc39d489dd3cc4e05f1cac" +dependencies = [ + "chrono", + "chrono-tz", + "lazy_static", + "log", + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 1.1.2+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokenizers" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476" +dependencies = [ + "ahash", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "fancy-regex", + "getrandom 0.3.4", + "hf-hub", + "itertools", + "log", + "macro_rules_attribute", + "monostate", + "paste", + "rand 0.9.4", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.18", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf8-zero", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.6", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..90a0cfd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +[workspace] +members = [ + "breadpad-shared", + "breadpad", + "breadman", + "breadpad-test", +] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT" +authors = ["Breadway"] + +[workspace.dependencies] +anyhow = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = { version = "1", features = ["v4"] } +chrono = { version = "0.4", features = ["serde"] } +rrule = "0.12" +tokio = { version = "1", features = ["full"] } +zbus = { version = "4", default-features = false, features = ["tokio"] } +ort = { version = "2.0.0-rc.12", features = ["download-binaries"] } +ndarray = "0.16" +tokenizers = { version = "0.21", default-features = false, features = ["http", "fancy-regex"] } +gtk4 = { version = "0.11", features = ["v4_12"] } +gtk4-layer-shell = "0.8" +hyprland = "0.4.0-beta.3" +toml = "0.8" +dirs = "5" +regex = "1" +ureq = { version = "2", features = ["json"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7feca3 --- /dev/null +++ b/README.md @@ -0,0 +1,414 @@ +# breadpad / breadman + +A quick-capture scratchpad and structured note viewer for Hyprland / Wayland, with AI-powered classification, reminders, recurrence, and snooze. + +Two entry points, one binary, one shared workspace: + +| Binary | Purpose | +|--------|---------| +| `breadpad` | Layer-shell capture popup — type a note, press Enter, done | +| `breadman` | Full note viewer and manager | + +--- + +## Workspace layout + +``` +breadpad-shared shared types, storage, classification, scheduler +breadpad GTK4 layer-shell capture popup +breadman GTK4 note viewer / manager +``` + +--- + +## Features + +### Capture (`breadpad`) + +- Layer-shell popup, centered, keyboard-exclusive — appears instantly on your keybind +- Single text field; press **Enter** or click **✓** to save, **Escape** to dismiss +- Optional manual type override before saving (defaults to AI classification) +- Timestamp and active Hyprland workspace recorded automatically + +### Classification + +Every note passes through a three-tier pipeline at capture time: + +1. **Rule-based parser** — always runs first; handles time extraction ("at 7pm", "in 30 minutes", "tomorrow morning", "next Friday"), recurrence ("every Sunday at 9pm", "every weekday morning"), and strong type signals ("?" → question, "idea:" prefix → idea, action verbs → todo). High-confidence results skip the remaining tiers entirely. +2. **Small local ONNX model** — runs when Tier 1 can't confidently assign a type. Responsible for type classification only; Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. +3. **Large local model via Ollama** — runs only when Tier 2 confidence falls below a configurable threshold. Communicates with a locally running Ollama instance over HTTP. If Ollama is unreachable, the Tier 2 result is used. No cloud APIs are involved. + +Manual override always available — the AI-assigned type is shown as a chip you can tap to change before saving. + +### Note types (built-in) + +| Type | Example | +|------|---------| +| `todo` | "buy milk on the way home" | +| `reminder` | "pack calculator in bag at 7pm" | +| `idea` | "what if breadman had a calendar view" | +| `note` | "meeting went well, follow up Friday" | +| `question` | "why does nmcli drop on suspend?" | + +User-defined tags can be added freely on top of the built-in types. + +### Reminders, recurrence, and snooze + +- **One-off reminders** — natural language time ("at 7pm", "in 30 minutes", "tomorrow morning") parsed at classification time; scheduled via a systemd user timer +- **Recurring reminders** — "every Sunday at 9pm", "every weekday morning" — stored as an iCal-compatible RRULE and re-scheduled on each trigger +- **Snooze** — notification popup includes snooze actions: 15 min / 1 hour / tomorrow morning / custom; snoozing reschedules the timer without touching the original note +- **Missed reminders** — if the system was off or suspended at the scheduled time, the reminder fires on next login + +### Viewer (`breadman`) + +- Sidebar with one entry per type + "All" and "Upcoming" +- Each note card shows: body, type chip, timestamp, workspace tag, recurrence badge if set +- **Upcoming** view: chronological list of all pending reminders and todos with times +- Inline editing — click any card to edit body, type, time, or recurrence +- Mark todo/reminder as done; done items move to an archive accessible via a toggle +- Search across all notes (full-text, instant) +- Sort by: newest, oldest, due time + +### Theming + +- Reads `~/.cache/wal/colors.json` (pywal) on startup — matches the rest of the bread ecosystem +- Falls back to Catppuccin Mocha +- CSS override: `~/.config/breadpad/style.css` +- `SIGHUP` reloads theme at runtime + +--- + +## Storage + +Notes are stored as JSONL at `~/.local/share/breadpad/notes.jsonl` — one JSON object per line, human-readable, easy to back up or script against. + +```jsonl +{"id":"a1b2c3","body":"Pack calculator in bag","type":"reminder","time":"2026-05-25T19:00:00","rrule":null,"done":false,"workspace":"1","created":"2026-05-25T18:45:00","snoozed_until":null} +{"id":"d4e5f6","body":"Look into relm4 reactive patterns","type":"idea","time":null,"rrule":null,"done":false,"workspace":"2","created":"2026-05-25T14:10:00","snoozed_until":null} +``` + +Completed notes are never deleted — they gain `"done": true` and a `"completed"` timestamp. A separate `~/.local/share/breadpad/archive.jsonl` is written periodically for notes older than 30 days. + +--- + +## AI classification + +### Three-tier pipeline + +#### Tier 1 — Rule-based parser + +Always runs. Handles: +- **Time extraction**: "at 7pm", "in 30 minutes", "tomorrow morning", "next Friday at 9am" +- **Recurrence**: "every Sunday at 9pm", "every weekday morning" → stored as RRULE +- **Type signals**: leading "?" or "why/how/what" → `question`; "idea:" prefix or "what if" → `idea`; action verbs → `todo`; time present → `reminder` + +Returns a calibrated confidence. If ≥ 0.82, Tiers 2 and 3 are skipped. + +#### Tier 2 — Small local ONNX model + +Runs when Tier 1 confidence is below threshold. Responsible for **type classification only** — Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. + +Invoked via `ort` (ONNX Runtime Rust bindings). Execution provider order: + +1. **QNN (Qualcomm/AMD XDNA NPU)** — requires `libQnnHtp.so` from the AMD Ryzen AI software stack +2. **Vulkan** — iGPU via the ONNX Runtime Vulkan EP +3. **CPU** — always available fallback + +Active provider shown in `breadpad --status`. + +#### Tier 3 — Large local model via Ollama + +Runs only when Tier 2 confidence falls below `model.ollama.confidence_threshold` (default 0.6). Sends a structured prompt to a locally running Ollama instance over HTTP and parses the JSON response for `type`, `body`, and `confidence`. The Ollama model runs on the iGPU via Ollama's own backend — breadpad does not manage GPU allocation for this tier. + +If Ollama is unreachable or returns an invalid response, breadpad logs a warning and uses the Tier 2 result. No cloud APIs are used anywhere. + +### Model location (Tier 2) + +``` +~/.local/share/breadpad/model/classifier.onnx +~/.local/share/breadpad/model/tokenizer.json +``` + +breadpad ships without a bundled model. Run `breadpad download-model` to fetch a recommended quantised model, or drop your own ONNX model in the above path. + +```bash +breadpad download-model # fetches default model (~150 MB) +breadpad model-info # shows active EP, model path, last inference time +``` + +--- + +## Requirements + +- Linux with a running Hyprland compositor +- GTK4 (≥ 4.12) + `gtk4-layer-shell` +- D-Bus session bus (for notifications) +- systemd user session (for timer-backed reminders) +- Rust 1.77+ +- ONNX Runtime (`ort` crate pulls this in automatically via the `download-binaries` feature) +- **NPU path only:** AMD Ryzen AI software stack (`amdxdna` kernel driver ≥ 6.11, QNN EP shared libs) +- **Tier 3 only (optional):** [Ollama](https://ollama.com) running locally with your chosen model pulled (`ollama pull llama3.2:3b`). Tier 3 is silently skipped if Ollama is not running. + +--- + +## Installation + +```bash +git clone https://github.com/breadway/breadpad +cd breadpad +cargo build --release +cp target/release/breadpad ~/.local/bin/ +cp target/release/breadman ~/.local/bin/ + +# Fetch the default classifier model +breadpad download-model +``` + +On Arch Linux, install GTK4 dependencies first: + +```bash +sudo pacman -S gtk4 gtk4-layer-shell +``` + +--- + +## Configuration + +On first run, breadpad writes `~/.config/breadpad/breadpad.toml`: + +```toml +[settings] +default_type = "note" # fallback type if classification is skipped +workspace_tag = true # tag notes with active Hyprland workspace +snooze_options = ["15m", "1h", "tomorrow_morning"] # shown in notification actions +archive_after_days = 30 + +[model] +path = "~/.local/share/breadpad/model/classifier.onnx" +tokenizer = "~/.local/share/breadpad/model/tokenizer.json" +execution_provider = "auto" # auto | npu | vulkan | cpu + +[model.ollama] +endpoint = "http://localhost:11434" +model = "llama3.2:3b" # any model you have pulled in Ollama +confidence_threshold = 0.6 # Tier 2 scores below this trigger Tier 3 +enabled = true # set false to never call Ollama + +[reminders] +default_morning = "08:00" # what "tomorrow morning" resolves to +missed_grace_minutes = 60 # how long after boot to still fire a missed reminder +``` + +--- + +## Usage + +### breadpad (capture) + +```bash +# Open the capture popup (bind this to a key in hyprland.conf) +breadpad + +# Open with a pre-selected type +breadpad --type todo + +# Skip AI classification (save as plain note) +breadpad --no-classify + +# Show model and storage status +breadpad --status +``` + +Hyprland keybind: + +``` +bind = $mainMod, N, exec, breadpad +``` + +### breadman (viewer) + +```bash +# Open the note viewer +breadman + +# Open directly to a specific type view +breadman --view todo +breadman --view upcoming + +# Mark a note done by ID (scriptable) +breadman done + +# List upcoming reminders in the terminal +breadman upcoming --plain +``` + +--- + +## Scheduler + +breadpad manages reminders via systemd user timers. Each scheduled note gets a transient timer unit: + +``` +breadpad-reminder-.timer +breadpad-reminder-.service +``` + +The service unit runs `breadpad fire `, which sends a `notify-send` notification with snooze actions. Snoozing writes the new time back to the note and creates a replacement timer. Recurring notes create the next timer immediately on fire. + +You can inspect pending timers: + +```bash +systemctl --user list-timers 'breadpad-*' +``` + +--- + +## Testing + +`breadpad-test` is a CLI test harness for the classification pipeline. It runs a JSON corpus of labelled inputs through any tier of the pipeline and reports pass/fail. + +```bash +# Run Tier 1 (rule-based only) — fast, no model needed +breadpad-test run + +# See only failing cases +breadpad-test run --format failures + +# Run Tier 2 (+ ONNX model) +breadpad-test run --tier 2 + +# Run full pipeline including Ollama +breadpad-test run --tier all + +# Machine-readable output +breadpad-test run --format json +``` + +### Corpus format + +Default path: `breadpad-test/corpus.json`. Override with `--corpus `. + +```json +[ + { + "input": "pack my calculator in my bag tonight", + "expected_type": "todo", + "expected_time": null, + "expected_body": "pack my calculator in my bag", + "expected_rrule": null, + "notes": "no time specified, should not infer one" + } +] +``` + +- `expected_time` — `HH:MM`; date component is ignored so tests are never date-sensitive +- `expected_rrule` — matched as substring of the actual RRULE string +- Any `null` field is skipped — only non-null fields are asserted + +### Tier modes + +| `--tier` | What runs | +|----------|-----------| +| `1` (default) | Tier 1 rule-based parser only — no model required | +| `2` | Tiers 1 + 2 (ONNX classifier) | +| `3` / `all` | Full pipeline including Tier 3 Ollama | + +### Corpus management + +```bash +# Interactively add an entry +breadpad-test add + +# Show entry #5 and the pipeline's actual output +breadpad-test show 5 + +# Open corpus file in $EDITOR at entry #5 +breadpad-test edit 5 +``` + +### Typical tuning workflow + +```bash +# 1. See what Tier 1 gets wrong +breadpad-test run --tier 1 --format failures + +# 2. Edit parser.rs, then rerun +cargo build -p breadpad-shared && breadpad-test run --tier 1 --format failures + +# 3. Once Tier 1 is stable, audit Tier 2 regressions +breadpad-test run --tier 2 --format failures +``` + +--- + +## Nextcloud Calendar integration + +breadpad can push scheduled notes and recurring reminders to a CalDAV calendar (Nextcloud or any RFC 4791-compliant server). No cloud APIs are used — everything goes directly to your own server over HTTPS. + +### What gets pushed + +Notes with a scheduled time (`time` field) or recurrence rule (`rrule`) are pushed as VEVENT entries when saved. Notes without a time are not pushed. Deleting a note also deletes the corresponding calendar event. + +### Configuration + +Add a `[calendar]` section to `~/.config/breadpad/breadpad.toml`: + +```toml +[calendar] +enabled = true +url = "https://nextcloud.example.com/remote.php/dav/calendars/you/breadpad/" +username = "you" +password = "app-password-here" # use a Nextcloud app password, not your login password +``` + +The calendar must already exist on the server. Create it in the Nextcloud Calendar app before enabling this integration. + +### CLI commands + +```bash +# Verify the CalDAV connection and credentials +breadpad calendar test + +# List CalDAV UIDs for all scheduled notes (queries the server if enabled, local store if not) +breadpad calendar list-uid + +# Show the CalDAV UID for a specific note by its local ID +breadpad calendar list-uid +``` + +### Event format + +Each note is pushed as a VEVENT with: +- `UID` — `@breadpad` (stable and deterministic) +- `SUMMARY` — note body +- `DTSTART` / `DTEND` — scheduled time (or creation time for recurring notes without a fixed start) +- `RRULE` — recurrence rule if set +- `DESCRIPTION` — `type=` + +### Security note + +Store your CalDAV password using a Nextcloud app password rather than your account password. App passwords can be revoked individually from the Nextcloud security settings. + +--- + +## Module layout + +| Crate / module | Responsibility | +|----------------|---------------| +| `breadpad-shared/src/types.rs` | `Note`, `NoteType`, `RecurrenceRule`, `SnoozeState` | +| `breadpad-shared/src/store.rs` | JSONL read/write, atomic saves, archive rotation | +| `breadpad-shared/src/classifier.rs` | Three-tier pipeline orchestration (Tier 1 → 2 → 3) | +| `breadpad-shared/src/parser.rs` | Tier 1: rule-based time/recurrence/type parsing | +| `breadpad-shared/src/ai.rs` | Tier 3: Ollama HTTP client, prompt construction, response parsing | +| `breadpad-shared/src/calendar.rs` | CalDAV client: push, delete, list events; iCal VEVENT builder | +| `breadpad-shared/src/scheduler.rs` | systemd timer creation, snooze, recurrence next-occurrence | +| `breadpad/src/main.rs` | GTK4 layer-shell popup, text field, type chip selector | +| `breadman/src/main.rs` | GTK4 app entry, sidebar, note list, search | +| `breadman/src/views/` | `upcoming.rs`, `archive.rs`, per-type list views | +| `breadman/src/editor.rs` | Inline note editor popover | + +--- + +## License + +MIT diff --git a/bread.zip b/bread.zip new file mode 100644 index 0000000000000000000000000000000000000000..9ce30677fa9e6a0c8d42347c4ea7bd03efbe1239 GIT binary patch literal 53251 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 literal 0 HcmV?d00001 diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml new file mode 100644 index 0000000..34c0a80 --- /dev/null +++ b/breadman/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "breadman" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[[bin]] +name = "breadman" +path = "src/main.rs" + +[dependencies] +breadpad-shared = { path = "../breadpad-shared" } +anyhow.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +chrono.workspace = true +gtk4.workspace = true +dirs.workspace = true diff --git a/breadman/src/editor.rs b/breadman/src/editor.rs new file mode 100644 index 0000000..60277b4 --- /dev/null +++ b/breadman/src/editor.rs @@ -0,0 +1,162 @@ +use breadpad_shared::{ + parser::parse_rule_based, + store::Store, + types::{Note, NoteType, RecurrenceRule}, +}; +use chrono::{Local, TimeZone, Utc}; +use gtk4::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +pub fn build_editor_popover( + note: &Note, + store: Arc, + morning: String, + on_save: impl Fn(Note) + 'static, + on_delete: impl Fn() + 'static, +) -> gtk4::Popover { + let popover = gtk4::Popover::new(); + popover.set_has_arrow(false); + + let vbox = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(8) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) + .width_request(420) + .build(); + + vbox.append(>k4::Label::builder().label("Body").xalign(0.0).build()); + let body_entry = gtk4::Entry::builder() + .text(¬e.body) + .hexpand(true) + .build(); + vbox.append(&body_entry); + + vbox.append(>k4::Label::builder().label("Type").xalign(0.0).build()); + let type_combo = gtk4::DropDown::from_strings(NoteType::all_builtin()); + let current_idx = NoteType::all_builtin() + .iter() + .position(|&s| s == note.note_type.as_str()) + .unwrap_or(3) as u32; + type_combo.set_selected(current_idx); + vbox.append(&type_combo); + + vbox.append(>k4::Label::builder().label("Time").xalign(0.0).build()); + let time_text = note + .time + .map(|t| { + let local: chrono::DateTime = t.into(); + local.format("%Y-%m-%d %H:%M").to_string() + }) + .unwrap_or_default(); + let time_entry = gtk4::Entry::builder() + .text(&time_text) + .placeholder_text("YYYY-MM-DD HH:MM or tomorrow 9am (blank = no time)") + .hexpand(true) + .build(); + vbox.append(&time_entry); + + vbox.append(>k4::Label::builder().label("Recurrence").xalign(0.0).build()); + let rrule_entry = gtk4::Entry::builder() + .text(note.rrule.as_ref().map(|r| r.as_str()).unwrap_or("")) + .placeholder_text("RRULE:FREQ=WEEKLY;BYDAY=MO (blank = none)") + .build(); + vbox.append(&rrule_entry); + + // Button row: [Delete] [Save] + let btn_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .build(); + + let delete_btn = gtk4::Button::builder() + .label("🗑 Delete") + .css_classes(["danger-btn"]) + .build(); + let save_btn = gtk4::Button::builder() + .label("Save") + .css_classes(["confirm-button"]) + .hexpand(true) + .build(); + btn_row.append(&delete_btn); + btn_row.append(&save_btn); + vbox.append(&btn_row); + + // Delete: two-click confirm using a single handler and shared state + let confirming = Rc::new(RefCell::new(false)); + { + let confirming = confirming.clone(); + let delete_btn_label = delete_btn.clone(); + let note_id = note.id.clone(); + let store_del = store.clone(); + let popover_del = popover.clone(); + + delete_btn.connect_clicked(move |_| { + let currently = *confirming.borrow(); + if currently { + if let Err(e) = store_del.delete_note(¬e_id) { + tracing::error!("failed to delete note: {}", e); + } else { + on_delete(); + } + popover_del.popdown(); + } else { + *confirming.borrow_mut() = true; + delete_btn_label.set_label("Sure?"); + } + }); + } + + // Save + let note_clone = note.clone(); + let popover_save = popover.clone(); + + save_btn.connect_clicked(move |_| { + let mut updated = note_clone.clone(); + updated.body = body_entry.text().to_string(); + + updated.note_type = NoteType::from_str( + NoteType::all_builtin() + .get(type_combo.selected() as usize) + .copied() + .unwrap_or("note"), + ); + + let time_str = time_entry.text().to_string(); + updated.time = if time_str.trim().is_empty() { + None + } else { + parse_time_field(&time_str, &morning) + }; + + let rrule_text = rrule_entry.text().to_string(); + updated.rrule = if rrule_text.trim().is_empty() { + None + } else { + Some(RecurrenceRule::new(rrule_text)) + }; + + if let Err(e) = store.update_note(&updated) { + tracing::error!("failed to update note: {}", e); + } else { + on_save(updated); + } + popover_save.popdown(); + }); + + popover.set_child(Some(&vbox)); + popover +} + +fn parse_time_field(s: &str, morning: &str) -> Option> { + if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s.trim(), "%Y-%m-%d %H:%M") { + if let chrono::LocalResult::Single(local) = Local.from_local_datetime(&naive) { + return Some(local.with_timezone(&Utc)); + } + } + parse_rule_based(s, morning).time +} diff --git a/breadman/src/main.rs b/breadman/src/main.rs new file mode 100644 index 0000000..386df78 --- /dev/null +++ b/breadman/src/main.rs @@ -0,0 +1,906 @@ +use anyhow::Result; +use breadpad_shared::{ + config::Config, + parser::parse_rule_based, + scheduler::Scheduler, + store::Store, + theme::{build_css, load_palette}, + types::{Note, NoteType, RecurrenceRule}, +}; +use chrono::Local; +use gtk4::{glib, prelude::*}; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +mod editor; +mod views; + +// ── Args ───────────────────────────────────────────────────────────────────── + +mod args { + #[derive(Debug)] + pub struct Args { + pub view: Option, + pub done_id: Option, + pub upcoming_plain: bool, + } + + pub fn parse() -> Args { + let mut args = Args { + view: None, + done_id: None, + upcoming_plain: false, + }; + let raw: Vec = std::env::args().skip(1).collect(); + let mut i = 0; + while i < raw.len() { + match raw[i].as_str() { + "--view" | "-v" => { + i += 1; + args.view = raw.get(i).cloned(); + } + "done" => { + i += 1; + args.done_id = raw.get(i).cloned(); + } + "upcoming" => { + if raw.get(i + 1).map(|s| s.as_str()) == Some("--plain") { + args.upcoming_plain = true; + i += 1; + } + args.view = Some("upcoming".into()); + } + _ => {} + } + i += 1; + } + args + } +} + +// ── AppState ────────────────────────────────────────────────────────────────── + +/// Shared UI state, cheap to clone (all fields are Rc/Arc). +#[derive(Clone)] +struct AppState { + store: Arc, + notes: Rc>>, + cfg: Rc>, + errors: Rc, String)>>>, + active_view: Rc>, + stack: gtk4::Stack, +} + +impl AppState { + fn new(store: Arc, notes: Vec, cfg: Config, stack: gtk4::Stack) -> Self { + AppState { + store, + notes: Rc::new(RefCell::new(notes)), + cfg: Rc::new(RefCell::new(cfg)), + errors: Rc::new(RefCell::new(Vec::new())), + active_view: Rc::new(RefCell::new("all".to_string())), + stack, + } + } + + fn log_error(&self, msg: impl Into) { + self.errors.borrow_mut().push((Local::now(), msg.into())); + } + + fn reload_notes(&self) { + match self.store.load_all() { + Ok(fresh) => *self.notes.borrow_mut() = fresh, + Err(e) => self.log_error(format!("failed to reload notes: {}", e)), + } + } + + /// Returns a Store clone with CalDAV wired in if enabled in config. + fn write_store(&self) -> Store { + let base = self.store.as_ref().clone(); + let cfg = self.cfg.borrow(); + if cfg.calendar.enabled { + base.with_calendar(cfg.calendar.clone()) + } else { + base + } + } +} + +// ── Refresh ─────────────────────────────────────────────────────────────────── + +fn refresh(state: &AppState) { + state.reload_notes(); + rebuild_stack(state); + let active = state.active_view.borrow().clone(); + state.stack.set_visible_child_name(&active); +} + +fn rebuild_stack(state: &AppState) { + while let Some(child) = state.stack.first_child() { + state.stack.remove(&child); + } + + let notes: Vec = state.notes.borrow().clone(); + let cfg: Config = state.cfg.borrow().clone(); + let errors: Vec<_> = state.errors.borrow().clone(); + + // All + let all_scroll = build_note_list(¬es, state.clone()); + state.stack.add_named(&all_scroll, Some("all")); + + // Upcoming + let upcoming = views::upcoming::build(¬es); + state.stack.add_named(&upcoming, Some("upcoming")); + + // Per-type + for type_name in NoteType::all_builtin() { + let nt = NoteType::from_str(type_name); + let filtered: Vec = notes + .iter() + .filter(|n| n.note_type == nt && !n.done) + .cloned() + .collect(); + let scroll = build_note_list(&filtered, state.clone()); + state.stack.add_named(&scroll, Some(type_name)); + } + + // Archive + let archive = views::archive::build(¬es, state.clone()); + state.stack.add_named(&archive, Some("archive")); + + // Settings + let state_s = state.clone(); + let settings = views::settings::build(&cfg, move |new_cfg| { + *state_s.cfg.borrow_mut() = new_cfg; + }); + state.stack.add_named(&settings, Some("settings")); + + // Errors + let errors_view = views::errors::build(&errors); + state.stack.add_named(&errors_view, Some("errors")); +} + +// ── main ───────────────────────────────────────────────────────────────────── + +fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("breadman=info".parse().unwrap()), + ) + .init(); + + let args = args::parse(); + let cfg = Config::load()?; + + if let Some(id) = &args.done_id { + return cmd_done(id); + } + if args.upcoming_plain { + return cmd_upcoming_plain(); + } + + run_app(args.view, cfg) +} + +fn cmd_done(id: &str) -> Result<()> { + let store = Store::new()?; + let note = match store.get_by_id(id)? { + Some(n) => n, + None => anyhow::bail!("note {} not found", id), + }; + let mut updated = note; + updated.mark_done(); + store.update_note(&updated)?; + println!("marked {} as done", id); + Ok(()) +} + +fn cmd_upcoming_plain() -> Result<()> { + let store = Store::new()?; + let mut notes: Vec = store + .load_all()? + .into_iter() + .filter(|n| { + !n.done + && matches!(n.note_type, NoteType::Reminder | NoteType::Todo) + && n.effective_time().is_some() + }) + .collect(); + notes.sort_by_key(|n| n.effective_time().unwrap()); + for note in ¬es { + let t = note.effective_time().unwrap(); + let local: chrono::DateTime = t.into(); + println!("[{}] {} — {}", note.id, local.format("%a %b %d %H:%M"), note.body); + } + Ok(()) +} + +fn run_app(initial_view: Option, cfg: Config) -> Result<()> { + let app = gtk4::Application::builder() + .application_id("com.breadway.breadman") + .build(); + + let cfg = Arc::new(cfg); + let initial_view = Arc::new(initial_view); + + app.connect_activate(move |app| { + let cfg = cfg.as_ref().clone(); + let initial_view = initial_view.as_deref().map(|s| s.to_string()); + if let Err(e) = build_app_window(app, cfg, initial_view) { + tracing::error!("failed to build window: {}", e); + } + }); + + let code = app.run_with_args::(&[]); + if code != glib::ExitCode::SUCCESS { + anyhow::bail!("GTK application exited with error"); + } + Ok(()) +} + +// ── Window ──────────────────────────────────────────────────────────────────── + +fn build_app_window( + app: >k4::Application, + cfg: Config, + initial_view: Option, +) -> Result<()> { + apply_css(&cfg); + + let store = Arc::new(Store::new()?); + let notes = store.load_all()?; + + let window = gtk4::ApplicationWindow::builder() + .application(app) + .title("breadman") + .default_width(960) + .default_height(640) + .build(); + + let hbox = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .build(); + + // ── Sidebar ─────────────────────────────────────────────────── + let sidebar_vbox = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .width_request(190) + .build(); + + let new_note_btn = gtk4::Button::builder() + .label("✚ New Note") + .css_classes(["confirm-button"]) + .margin_start(10) + .margin_end(10) + .margin_top(12) + .margin_bottom(6) + .build(); + sidebar_vbox.append(&new_note_btn); + + let sidebar_list = gtk4::ListBox::builder() + .selection_mode(gtk4::SelectionMode::Single) + .css_classes(["sidebar"]) + .build(); + + let make_section = |title: &str| { + let row = gtk4::ListBoxRow::builder() + .selectable(false) + .activatable(false) + .build(); + row.set_child(Some( + >k4::Label::builder() + .label(title) + .xalign(0.0) + .css_classes(["sidebar-section-label"]) + .build(), + )); + row + }; + let make_item = |id: &str, icon: &str, label: &str| { + let row = gtk4::ListBoxRow::builder() + .css_classes(["sidebar-row"]) + .build(); + row.set_widget_name(id); + let hbox = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(10) + .build(); + hbox.append( + >k4::Label::builder() + .label(icon) + .width_chars(2) + .xalign(0.5) + .build(), + ); + hbox.append( + >k4::Label::builder() + .label(label) + .xalign(0.0) + .hexpand(true) + .build(), + ); + row.set_child(Some(&hbox)); + row + }; + + sidebar_list.append(&make_section("VIEWS")); + sidebar_list.append(&make_item("all", "📋", "All")); + sidebar_list.append(&make_item("upcoming", "📅", "Upcoming")); + sidebar_list.append(&make_section("TYPES")); + sidebar_list.append(&make_item("todo", "✅", "Todo")); + sidebar_list.append(&make_item("reminder", "🔔", "Reminder")); + sidebar_list.append(&make_item("idea", "💡", "Idea")); + sidebar_list.append(&make_item("note", "📝", "Note")); + sidebar_list.append(&make_item("question", "❓", "Question")); + sidebar_list.append(&make_section("MORE")); + sidebar_list.append(&make_item("archive", "📦", "Archive")); + sidebar_list.append(&make_item("settings", "⚙", "Settings")); + sidebar_list.append(&make_item("errors", "⚠", "Errors")); + sidebar_vbox.append(&sidebar_list); + + // ── Content area ────────────────────────────────────────────── + let content_vbox = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .hexpand(true) + .build(); + + let search_entry = gtk4::SearchEntry::builder() + .placeholder_text("Search notes…") + .css_classes(["search-entry"]) + .margin_start(8) + .margin_end(8) + .margin_top(8) + .margin_bottom(4) + .build(); + + let stack = gtk4::Stack::builder().hexpand(true).vexpand(true).build(); + + content_vbox.append(&search_entry); + content_vbox.append(&stack); + + hbox.append(&sidebar_vbox); + hbox.append(>k4::Separator::builder() + .orientation(gtk4::Orientation::Vertical) + .build()); + hbox.append(&content_vbox); + window.set_child(Some(&hbox)); + + // ── AppState ────────────────────────────────────────────────── + let state = AppState::new(store, notes, cfg, stack.clone()); + + // Initial build + rebuild_stack(&state); + + // ── Sidebar selection ───────────────────────────────────────── + { + let state_c = state.clone(); + sidebar_list.connect_row_selected(move |_, row| { + if let Some(row) = row { + let view = row.widget_name().to_string(); + if view.is_empty() { return; } + *state_c.active_view.borrow_mut() = view.clone(); + refresh(&state_c); + } + }); + } + + // ── Search ──────────────────────────────────────────────────── + { + let state_c = state.clone(); + search_entry.connect_search_changed(move |entry| { + let query = entry.text().to_string(); + let all_notes = state_c.notes.borrow().clone(); + let filtered: Vec = if query.trim().is_empty() { + all_notes + } else { + let q = query.to_lowercase(); + all_notes + .into_iter() + .filter(|n| n.body.to_lowercase().contains(&q)) + .collect() + }; + + // Replace the "all" page with the filtered list while preserving others + while let Some(child) = state_c.stack.first_child() { + state_c.stack.remove(&child); + } + let all_scroll = build_note_list(&filtered, state_c.clone()); + state_c.stack.add_named(&all_scroll, Some("all")); + + let notes_snap = state_c.notes.borrow().clone(); + let cfg_snap = state_c.cfg.borrow().clone(); + let errors_snap = state_c.errors.borrow().clone(); + + let upcoming = views::upcoming::build(¬es_snap); + state_c.stack.add_named(&upcoming, Some("upcoming")); + for type_name in NoteType::all_builtin() { + let nt = NoteType::from_str(type_name); + let typed: Vec = notes_snap + .iter() + .filter(|n| n.note_type == nt && !n.done) + .cloned() + .collect(); + state_c.stack.add_named(&build_note_list(&typed, state_c.clone()), Some(type_name)); + } + state_c.stack.add_named(&views::archive::build(¬es_snap, state_c.clone()), Some("archive")); + let state_s = state_c.clone(); + state_c.stack.add_named( + &views::settings::build(&cfg_snap, move |nc| { *state_s.cfg.borrow_mut() = nc; }), + Some("settings"), + ); + state_c.stack.add_named(&views::errors::build(&errors_snap), Some("errors")); + state_c.stack.set_visible_child_name("all"); + }); + } + + // ── New Note button ─────────────────────────────────────────── + { + let state_c = state.clone(); + let window_c = window.clone(); + new_note_btn.connect_clicked(move |_| { + show_add_note_window(&window_c, state_c.clone()); + }); + } + + // ── Select initial view ─────────────────────────────────────── + let initial = initial_view.as_deref().unwrap_or("all"); + *state.active_view.borrow_mut() = initial.to_string(); + for row in sidebar_list + .observe_children() + .snapshot() + .iter() + .filter_map(|o| o.clone().downcast::().ok()) + { + if row.widget_name() == initial { + sidebar_list.select_row(Some(&row)); + break; + } + } + stack.set_visible_child_name(initial); + + window.present(); + Ok(()) +} + +// ── Note list & cards ───────────────────────────────────────────────────────── + +fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow { + let scroll = gtk4::ScrolledWindow::builder() + .hscrollbar_policy(gtk4::PolicyType::Never) + .vscrollbar_policy(gtk4::PolicyType::Automatic) + .vexpand(true) + .build(); + + let list = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(4) + .margin_top(8) + .margin_bottom(8) + .build(); + + let mut sorted: Vec = notes.iter().filter(|n| !n.done).cloned().collect(); + sorted.sort_by(|a, b| b.created.cmp(&a.created)); + + if sorted.is_empty() { + list.append( + >k4::Label::builder() + .label("No notes here yet.") + .margin_top(32) + .build(), + ); + } else { + for note in &sorted { + list.append(&build_note_card(note, state.clone())); + } + } + + scroll.set_child(Some(&list)); + scroll +} + +fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { + let card = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(4) + .margin_start(8) + .margin_end(8) + .margin_top(4) + .margin_bottom(4) + .css_classes(["note-card"]) + .build(); + card.add_css_class(&format!("note-card-{}", note.note_type.as_str())); + + // Top row: body + type chip + let top_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .build(); + + let body_label = gtk4::Label::builder() + .label(¬e.body) + .hexpand(true) + .xalign(0.0) + .wrap(true) + .build(); + + let type_chip = gtk4::Label::builder() + .label(note.note_type.as_str()) + .css_classes(["type-chip"]) + .build(); + + top_row.append(&body_label); + top_row.append(&type_chip); + + // Bottom row: metadata + action buttons + let bottom_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .build(); + + let created_str = { + let local: chrono::DateTime = note.created.into(); + local.format("%b %d %H:%M").to_string() + }; + let meta_label = gtk4::Label::builder() + .label(&created_str) + .css_classes(["dim-label"]) + .xalign(0.0) + .build(); + + // Date first, then chips + bottom_row.append(&meta_label); + if let Some(ws) = ¬e.workspace { + bottom_row.append( + >k4::Label::builder() + .label(&format!("ws:{}", ws)) + .css_classes(["type-chip"]) + .build(), + ); + } + if let Some(t) = note.time { + let local: chrono::DateTime = t.into(); + bottom_row.append( + >k4::Label::builder() + .label(&local.format("⏰ %b %d %H:%M").to_string()) + .css_classes(["dim-label"]) + .build(), + ); + } + if note.rrule.is_some() { + bottom_row.append( + >k4::Label::builder() + .label("↻") + .css_classes(["type-chip"]) + .build(), + ); + } + + bottom_row.append(>k4::Box::builder().hexpand(true).build()); + + // ✓ Done button + let done_btn = gtk4::Button::builder() + .label("✓") + .css_classes(["action-btn", "done-btn"]) + .tooltip_text("Mark done") + .build(); + { + let note_id = note.id.clone(); + let card_c = card.clone(); + let state_c = state.clone(); + done_btn.connect_clicked(move |_| { + if let Ok(Some(mut n)) = state_c.store.get_by_id(¬e_id) { + n.mark_done(); + if let Err(e) = state_c.store.update_note(&n) { + state_c.log_error(format!("mark done failed: {}", e)); + } + } + card_c.set_visible(false); + state_c.reload_notes(); + }); + } + bottom_row.append(&done_btn); + + // ✎ Edit button + let edit_btn = gtk4::Button::builder() + .label("✎") + .css_classes(["action-btn", "edit-btn"]) + .tooltip_text("Edit") + .build(); + { + let note_c = note.clone(); + let state_c = state.clone(); + let body_label_c = body_label.clone(); + let card_c = card.clone(); + + edit_btn.connect_clicked(move |btn| { + let morning = state_c.cfg.borrow().reminders.default_morning.clone(); + let store = Arc::new(state_c.write_store()); + + let state_save = state_c.clone(); + let body_label_save = body_label_c.clone(); + let state_del = state_c.clone(); + let card_del = card_c.clone(); + + let popover = editor::build_editor_popover( + ¬e_c, + store, + morning, + move |updated: Note| { + body_label_save.set_label(&updated.body); + state_save.reload_notes(); + }, + move || { + card_del.set_visible(false); + state_del.reload_notes(); + }, + ); + popover.set_parent(btn); + popover.popup(); + }); + } + bottom_row.append(&edit_btn); + + // 🗑 Delete button — two-click confirm: first click → "Sure?", second → delete + let delete_btn = gtk4::Button::builder() + .label("🗑") + .css_classes(["action-btn", "danger-btn"]) + .tooltip_text("Delete") + .build(); + { + use std::cell::RefCell; + use std::rc::Rc; + let confirming = Rc::new(RefCell::new(false)); + let note_id = note.id.clone(); + let card_c = card.clone(); + let state_c = state.clone(); + let btn_c = delete_btn.clone(); + + delete_btn.connect_clicked(move |_| { + if *confirming.borrow() { + let store = state_c.write_store(); + if let Err(e) = store.delete_note(¬e_id) { + state_c.log_error(format!("delete failed: {}", e)); + } + card_c.set_visible(false); + state_c.reload_notes(); + } else { + *confirming.borrow_mut() = true; + btn_c.set_label("Sure?"); + } + }); + } + bottom_row.append(&delete_btn); + + card.append(&top_row); + card.append(&bottom_row); + card +} + +// ── Add note window ─────────────────────────────────────────────────────────── + +fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) { + let win = gtk4::Window::builder() + .title("New Note") + .transient_for(parent) + .modal(true) + .default_width(500) + .build(); + + let vbox = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(10) + .margin_top(16) + .margin_bottom(16) + .margin_start(16) + .margin_end(16) + .build(); + + vbox.append(>k4::Label::builder().label("Body").xalign(0.0).build()); + let body_entry = gtk4::Entry::builder() + .placeholder_text("What's on your mind?") + .hexpand(true) + .build(); + vbox.append(&body_entry); + + // Type chips + let chip_box = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(4) + .build(); + let selected_type: Rc> = Rc::new(RefCell::new(NoteType::Note)); + let chips: Vec<(gtk4::Button, NoteType)> = NoteType::all_builtin() + .iter() + .map(|&name| { + let btn = gtk4::Button::builder() + .label(name) + .css_classes(["type-chip"]) + .build(); + (btn, NoteType::from_str(name)) + }) + .collect(); + for (btn, nt) in &chips { + let sel = selected_type.clone(); + let nt_c = nt.clone(); + let all_btns: Vec = chips.iter().map(|(b, _)| b.clone()).collect(); + btn.connect_clicked(move |clicked| { + *sel.borrow_mut() = nt_c.clone(); + for b in &all_btns { b.remove_css_class("active"); } + clicked.add_css_class("active"); + }); + chip_box.append(btn); + } + if let Some((btn, _)) = chips.iter().find(|(_, nt)| *nt == NoteType::Note) { + btn.add_css_class("active"); + } + vbox.append(&chip_box); + + vbox.append(>k4::Label::builder().label("Time (optional)").xalign(0.0).build()); + let time_entry = gtk4::Entry::builder() + .placeholder_text("tomorrow 9am / at 7pm / in 30 minutes") + .hexpand(true) + .build(); + vbox.append(&time_entry); + + vbox.append(>k4::Label::builder().label("Recurrence (optional)").xalign(0.0).build()); + let rrule_entry = gtk4::Entry::builder() + .placeholder_text("RRULE:FREQ=WEEKLY;BYDAY=MO") + .hexpand(true) + .build(); + vbox.append(&rrule_entry); + + let status_label = gtk4::Label::builder() + .label("") + .xalign(0.0) + .css_classes(["dim-label"]) + .build(); + vbox.append(&status_label); + + let btn_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .build(); + let cancel_btn = gtk4::Button::builder().label("Cancel").build(); + let add_btn = gtk4::Button::builder() + .label("Add Note") + .css_classes(["confirm-button"]) + .build(); + btn_row.append(>k4::Box::builder().hexpand(true).build()); + btn_row.append(&cancel_btn); + btn_row.append(&add_btn); + vbox.append(&btn_row); + + win.set_child(Some(&vbox)); + + // Cancel + { + let win_c = win.clone(); + cancel_btn.connect_clicked(move |_| win_c.close()); + } + + // Add Note + { + let win_c = win.clone(); + let state_c = state.clone(); + let body_c = body_entry.clone(); + let time_c = time_entry.clone(); + let rrule_c = rrule_entry.clone(); + let sel_c = selected_type.clone(); + let status_c = status_label.clone(); + + let do_add = move || { + let body_text = body_c.text().to_string(); + if body_text.trim().is_empty() { + status_c.set_label("Body is required."); + return; + } + + let morning = state_c.cfg.borrow().reminders.default_morning.clone(); + + // Tier 1 classification on body + let parsed = parse_rule_based(&body_text, &morning); + + let user_type = sel_c.borrow().clone(); + let default_type = NoteType::from_str(&state_c.cfg.borrow().settings.default_type); + + let mut note = Note::new(parsed.body.clone(), user_type.clone(), None); + // Use parsed type if user left it at the default + if user_type == default_type { + note.note_type = parsed.note_type; + } + note.time = parsed.time; + note.rrule = parsed.rrule; + + // Time field overrides + let time_str = time_c.text().to_string(); + if !time_str.trim().is_empty() { + let tp = parse_rule_based(&time_str, &morning); + if tp.time.is_some() { note.time = tp.time; } + if tp.rrule.is_some() { note.rrule = tp.rrule; } + } + + // RRULE field overrides + let rrule_str = rrule_c.text().to_string(); + if !rrule_str.trim().is_empty() { + note.rrule = Some(RecurrenceRule::new(rrule_str)); + } + + let store = state_c.write_store(); + if let Err(e) = store.save_note(¬e) { + state_c.log_error(format!("save failed: {}", e)); + return; + } + if note.time.is_some() { + if let Err(e) = Scheduler::schedule(¬e) { + state_c.log_error(format!("schedule failed: {}", e)); + } + } + + win_c.close(); + // Defer refresh so the window close event is processed first + let state_refresh = state_c.clone(); + glib::idle_add_local_once(move || refresh(&state_refresh)); + }; + + add_btn.connect_clicked(move |_| do_add()); + } + + // Also trigger add on Enter in body field + { + let win_c2 = win.clone(); + let state_c2 = state.clone(); + let body_c2 = body_entry.clone(); + let time_c2 = time_entry.clone(); + let rrule_c2 = rrule_entry.clone(); + let sel_c2 = selected_type.clone(); + + body_entry.connect_activate(move |_| { + // If time/rrule fields are empty, submit immediately + if time_c2.text().is_empty() && rrule_c2.text().is_empty() { + let body_text = body_c2.text().to_string(); + if body_text.trim().is_empty() { return; } + let morning = state_c2.cfg.borrow().reminders.default_morning.clone(); + let parsed = parse_rule_based(&body_text, &morning); + let user_type = sel_c2.borrow().clone(); + let default_type = NoteType::from_str(&state_c2.cfg.borrow().settings.default_type); + let mut note = Note::new(parsed.body.clone(), user_type.clone(), None); + if user_type == default_type { note.note_type = parsed.note_type; } + note.time = parsed.time; + note.rrule = parsed.rrule; + let store = state_c2.write_store(); + if let Err(e) = store.save_note(¬e) { + state_c2.log_error(format!("save failed: {}", e)); + return; + } + if note.time.is_some() { + if let Err(e) = Scheduler::schedule(¬e) { + state_c2.log_error(format!("schedule failed: {}", e)); + } + } + win_c2.close(); + let sr = state_c2.clone(); + glib::idle_add_local_once(move || refresh(&sr)); + } + }); + } + + win.present(); + body_entry.grab_focus(); +} + +// ── CSS ─────────────────────────────────────────────────────────────────────── + +fn apply_css(_cfg: &Config) { + let palette = load_palette(); + let user_css = std::fs::read_to_string(breadpad_shared::config::style_css_path()).ok(); + let css = build_css(&palette, user_css.as_deref()); + + let provider = gtk4::CssProvider::new(); + provider.load_from_string(&css); + gtk4::style_context_add_provider_for_display( + >k4::gdk::Display::default().unwrap(), + &provider, + gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); +} diff --git a/breadman/src/views/archive.rs b/breadman/src/views/archive.rs new file mode 100644 index 0000000..b4cd75f --- /dev/null +++ b/breadman/src/views/archive.rs @@ -0,0 +1,109 @@ +use breadpad_shared::types::Note; +use gtk4::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +pub fn build(notes: &[Note], state: crate::AppState) -> gtk4::ScrolledWindow { + let scroll = gtk4::ScrolledWindow::builder() + .hscrollbar_policy(gtk4::PolicyType::Never) + .vscrollbar_policy(gtk4::PolicyType::Automatic) + .vexpand(true) + .build(); + + let list = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(4) + .margin_top(8) + .margin_bottom(8) + .build(); + + let mut archived: Vec<&Note> = notes.iter().filter(|n| n.done).collect(); + archived.sort_by(|a, b| b.created.cmp(&a.created)); + + if archived.is_empty() { + list.append( + >k4::Label::builder() + .label("Archive is empty.") + .margin_top(32) + .build(), + ); + } else { + for note in archived { + list.append(&build_archive_card(note, state.clone())); + } + } + + scroll.set_child(Some(&list)); + scroll +} + +fn build_archive_card(note: &Note, state: crate::AppState) -> gtk4::Box { + let row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .margin_start(8) + .margin_end(8) + .margin_top(2) + .margin_bottom(2) + .css_classes(["note-card"]) + .build(); + + let completed_str = note + .completed + .map(|t| { + let local: chrono::DateTime = t.into(); + format!("done {}", local.format("%b %d")) + }) + .unwrap_or_else(|| "done".into()); + + let done_label = gtk4::Label::builder() + .label(&completed_str) + .width_chars(12) + .xalign(0.0) + .build(); + + let body_label = gtk4::Label::builder() + .label(¬e.body) + .hexpand(true) + .xalign(0.0) + .ellipsize(gtk4::pango::EllipsizeMode::End) + .build(); + + let type_label = gtk4::Label::builder() + .label(note.note_type.as_str()) + .css_classes(["type-chip"]) + .build(); + + // 🗑 Delete — two-click confirm + let delete_btn = gtk4::Button::builder() + .label("🗑") + .css_classes(["action-btn", "danger-btn"]) + .tooltip_text("Delete permanently") + .build(); + { + let confirming = Rc::new(RefCell::new(false)); + let note_id = note.id.clone(); + let row_c = row.clone(); + let btn_c = delete_btn.clone(); + + delete_btn.connect_clicked(move |_| { + if *confirming.borrow() { + let store = state.write_store(); + if let Err(e) = store.delete_note(¬e_id) { + state.log_error(format!("delete failed: {}", e)); + } + row_c.set_visible(false); + state.reload_notes(); + } else { + *confirming.borrow_mut() = true; + btn_c.set_label("Sure?"); + } + }); + } + + row.append(&done_label); + row.append(&body_label); + row.append(&type_label); + row.append(&delete_btn); + row +} diff --git a/breadman/src/views/errors.rs b/breadman/src/views/errors.rs new file mode 100644 index 0000000..1be64e0 --- /dev/null +++ b/breadman/src/views/errors.rs @@ -0,0 +1,60 @@ +use chrono::DateTime; +use gtk4::prelude::*; + +pub fn build(entries: &[(DateTime, String)]) -> gtk4::ScrolledWindow { + let scroll = gtk4::ScrolledWindow::builder() + .hscrollbar_policy(gtk4::PolicyType::Never) + .vscrollbar_policy(gtk4::PolicyType::Automatic) + .vexpand(true) + .build(); + + let list = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(4) + .margin_top(8) + .margin_bottom(8) + .margin_start(8) + .margin_end(8) + .build(); + + if entries.is_empty() { + list.append( + >k4::Label::builder() + .label("No errors or warnings this session.") + .margin_top(32) + .build(), + ); + } else { + for (ts, msg) in entries.iter().rev() { + let row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .css_classes(["note-card"]) + .margin_top(2) + .margin_bottom(2) + .build(); + + let time_label = gtk4::Label::builder() + .label(&ts.format("%H:%M:%S").to_string()) + .width_chars(10) + .xalign(0.0) + .css_classes(["dim-label"]) + .build(); + + let msg_label = gtk4::Label::builder() + .label(msg) + .hexpand(true) + .xalign(0.0) + .wrap(true) + .selectable(true) + .build(); + + row.append(&time_label); + row.append(&msg_label); + list.append(&row); + } + } + + scroll.set_child(Some(&list)); + scroll +} diff --git a/breadman/src/views/mod.rs b/breadman/src/views/mod.rs new file mode 100644 index 0000000..d4cde84 --- /dev/null +++ b/breadman/src/views/mod.rs @@ -0,0 +1,4 @@ +pub mod archive; +pub mod errors; +pub mod settings; +pub mod upcoming; diff --git a/breadman/src/views/settings.rs b/breadman/src/views/settings.rs new file mode 100644 index 0000000..d110dd4 --- /dev/null +++ b/breadman/src/views/settings.rs @@ -0,0 +1,271 @@ +use breadpad_shared::config::{ + CalendarConfig, Config, ModelConfig, OllamaConfig, RemindersConfig, Settings, +}; +use gtk4::prelude::*; + +pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::ScrolledWindow { + let scroll = gtk4::ScrolledWindow::builder() + .hscrollbar_policy(gtk4::PolicyType::Never) + .vscrollbar_policy(gtk4::PolicyType::Automatic) + .vexpand(true) + .build(); + + let outer = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(16) + .margin_top(16) + .margin_bottom(16) + .margin_start(16) + .margin_end(16) + .build(); + + // ── General ────────────────────────────────────────────────── + let (general_frame, general_grid) = make_section("General"); + + let type_options = ["note", "todo", "reminder", "idea", "question"]; + let default_type_combo = gtk4::DropDown::from_strings(&type_options); + let dt_idx = type_options + .iter() + .position(|&s| s == cfg.settings.default_type.as_str()) + .unwrap_or(0) as u32; + default_type_combo.set_selected(dt_idx); + attach_row(&general_grid, 0, "Default type", &default_type_combo); + + let ws_tag_switch = gtk4::Switch::builder() + .active(cfg.settings.workspace_tag) + .valign(gtk4::Align::Center) + .build(); + attach_row(&general_grid, 1, "Workspace tag", &ws_tag_switch); + + let archive_spin = gtk4::SpinButton::with_range(1.0, 365.0, 1.0); + archive_spin.set_value(cfg.settings.archive_after_days as f64); + attach_row(&general_grid, 2, "Archive after (days)", &archive_spin); + + let snooze_entry = gtk4::Entry::builder() + .text(&cfg.settings.snooze_options.join(", ")) + .hexpand(true) + .build(); + attach_row(&general_grid, 3, "Snooze options", &snooze_entry); + + outer.append(&general_frame); + + // ── Reminders ──────────────────────────────────────────────── + let (rem_frame, rem_grid) = make_section("Reminders"); + + let morning_entry = gtk4::Entry::builder() + .text(&cfg.reminders.default_morning) + .placeholder_text("HH:MM") + .build(); + attach_row(&rem_grid, 0, "Default morning", &morning_entry); + + let grace_spin = gtk4::SpinButton::with_range(0.0, 1440.0, 5.0); + grace_spin.set_value(cfg.reminders.missed_grace_minutes as f64); + attach_row(&rem_grid, 1, "Missed grace (minutes)", &grace_spin); + + outer.append(&rem_frame); + + // ── Model ───────────────────────────────────────────────────── + let (model_frame, model_grid) = make_section("Model (Tier 2 ONNX)"); + + let model_path_entry = gtk4::Entry::builder() + .text(&cfg.model.path) + .hexpand(true) + .build(); + attach_row(&model_grid, 0, "ONNX path", &model_path_entry); + + let tokenizer_entry = gtk4::Entry::builder() + .text(&cfg.model.tokenizer) + .hexpand(true) + .build(); + attach_row(&model_grid, 1, "Tokenizer path", &tokenizer_entry); + + let ep_options = ["auto", "npu", "vulkan", "cpu"]; + let ep_combo = gtk4::DropDown::from_strings(&ep_options); + let ep_idx = ep_options + .iter() + .position(|&s| s == cfg.model.execution_provider.as_str()) + .unwrap_or(0) as u32; + ep_combo.set_selected(ep_idx); + attach_row(&model_grid, 2, "Execution provider", &ep_combo); + + outer.append(&model_frame); + + // ── Ollama (Tier 3) ─────────────────────────────────────────── + let (ollama_frame, ollama_grid) = make_section("Ollama (Tier 3)"); + + let ollama_enabled = gtk4::Switch::builder() + .active(cfg.model.ollama.enabled) + .valign(gtk4::Align::Center) + .build(); + attach_row(&ollama_grid, 0, "Enabled", &ollama_enabled); + + let ollama_endpoint = gtk4::Entry::builder() + .text(&cfg.model.ollama.endpoint) + .hexpand(true) + .build(); + attach_row(&ollama_grid, 1, "Endpoint", &ollama_endpoint); + + let ollama_model = gtk4::Entry::builder() + .text(&cfg.model.ollama.model) + .build(); + attach_row(&ollama_grid, 2, "Model", &ollama_model); + + let ollama_thresh = gtk4::SpinButton::with_range(0.0, 1.0, 0.05); + ollama_thresh.set_value(cfg.model.ollama.confidence_threshold as f64); + ollama_thresh.set_digits(2); + attach_row(&ollama_grid, 3, "Confidence threshold", &ollama_thresh); + + outer.append(&ollama_frame); + + // ── Calendar ───────────────────────────────────────────────── + let (cal_frame, cal_grid) = make_section("Nextcloud Calendar (CalDAV)"); + + let cal_enabled = gtk4::Switch::builder() + .active(cfg.calendar.enabled) + .valign(gtk4::Align::Center) + .build(); + attach_row(&cal_grid, 0, "Enabled", &cal_enabled); + + let cal_url = gtk4::Entry::builder() + .text(&cfg.calendar.url) + .placeholder_text("https://nextcloud.example.com/remote.php/dav/calendars/you/personal/") + .hexpand(true) + .build(); + attach_row(&cal_grid, 1, "Calendar URL", &cal_url); + + let cal_user = gtk4::Entry::builder() + .text(&cfg.calendar.username) + .build(); + attach_row(&cal_grid, 2, "Username", &cal_user); + + let cal_pass = gtk4::Entry::builder() + .text(&cfg.calendar.password) + .input_purpose(gtk4::InputPurpose::Password) + .visibility(false) + .build(); + attach_row(&cal_grid, 3, "App password", &cal_pass); + + outer.append(&cal_frame); + + // ── Save ────────────────────────────────────────────────────── + let status_label = gtk4::Label::builder() + .label("") + .xalign(0.0) + .css_classes(["dim-label"]) + .build(); + let save_btn = gtk4::Button::builder() + .label("Save Settings") + .css_classes(["confirm-button"]) + .halign(gtk4::Align::End) + .build(); + + { + let dtc = default_type_combo.clone(); + let wts = ws_tag_switch.clone(); + let ars = archive_spin.clone(); + let sne = snooze_entry.clone(); + let moe = morning_entry.clone(); + let grs = grace_spin.clone(); + let mpe = model_path_entry.clone(); + let tke = tokenizer_entry.clone(); + let epc = ep_combo.clone(); + let oec = ollama_enabled.clone(); + let oee = ollama_endpoint.clone(); + let ome = ollama_model.clone(); + let ots = ollama_thresh.clone(); + let cec = cal_enabled.clone(); + let cuc = cal_url.clone(); + let csc = cal_user.clone(); + let cpc = cal_pass.clone(); + let sl = status_label.clone(); + + save_btn.connect_clicked(move |_| { + let new_cfg = Config { + settings: Settings { + default_type: type_options + .get(dtc.selected() as usize) + .copied() + .unwrap_or("note") + .to_string(), + workspace_tag: wts.is_active(), + snooze_options: sne + .text() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(), + archive_after_days: ars.value() as i64, + }, + reminders: RemindersConfig { + default_morning: moe.text().to_string(), + missed_grace_minutes: grs.value() as i64, + }, + model: ModelConfig { + path: mpe.text().to_string(), + tokenizer: tke.text().to_string(), + execution_provider: ep_options + .get(epc.selected() as usize) + .copied() + .unwrap_or("auto") + .to_string(), + ollama: OllamaConfig { + enabled: oec.is_active(), + endpoint: oee.text().to_string(), + model: ome.text().to_string(), + confidence_threshold: ots.value() as f32, + }, + }, + calendar: CalendarConfig { + enabled: cec.is_active(), + url: cuc.text().to_string(), + username: csc.text().to_string(), + password: cpc.text().to_string(), + }, + }; + match new_cfg.save() { + Ok(()) => { + sl.set_label("Settings saved."); + on_save(new_cfg); + } + Err(e) => sl.set_label(&format!("Save failed: {}", e)), + } + }); + } + + let btn_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .build(); + btn_row.append(&status_label); + btn_row.append(>k4::Box::builder().hexpand(true).build()); + btn_row.append(&save_btn); + outer.append(&btn_row); + + scroll.set_child(Some(&outer)); + scroll +} + +fn make_section(title: &str) -> (gtk4::Frame, gtk4::Grid) { + let frame = gtk4::Frame::builder().label(title).build(); + let grid = gtk4::Grid::builder() + .row_spacing(8) + .column_spacing(16) + .margin_top(8) + .margin_bottom(8) + .margin_start(8) + .margin_end(8) + .build(); + frame.set_child(Some(&grid)); + (frame, grid) +} + +fn attach_row(grid: >k4::Grid, row: i32, label: &str, widget: &impl gtk4::prelude::IsA) { + let lbl = gtk4::Label::builder() + .label(label) + .xalign(0.0) + .hexpand(false) + .width_chars(24) + .build(); + grid.attach(&lbl, 0, row, 1, 1); + grid.attach(widget, 1, row, 1, 1); +} diff --git a/breadman/src/views/upcoming.rs b/breadman/src/views/upcoming.rs new file mode 100644 index 0000000..35480c0 --- /dev/null +++ b/breadman/src/views/upcoming.rs @@ -0,0 +1,86 @@ +use breadpad_shared::types::{Note, NoteType}; +use gtk4::prelude::*; + +pub fn build(notes: &[Note]) -> gtk4::ScrolledWindow { + let scroll = gtk4::ScrolledWindow::builder() + .hscrollbar_policy(gtk4::PolicyType::Never) + .vscrollbar_policy(gtk4::PolicyType::Automatic) + .vexpand(true) + .build(); + + let list = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(4) + .margin_top(8) + .margin_bottom(8) + .build(); + + let mut upcoming: Vec<&Note> = notes + .iter() + .filter(|n| { + !n.done + && matches!(n.note_type, NoteType::Reminder | NoteType::Todo) + && n.effective_time().is_some() + }) + .collect(); + upcoming.sort_by_key(|n| n.effective_time().unwrap()); + + if upcoming.is_empty() { + let label = gtk4::Label::builder() + .label("No upcoming reminders or todos.") + .margin_top(32) + .build(); + list.append(&label); + } else { + for note in upcoming { + let card = build_upcoming_card(note); + list.append(&card); + } + } + + scroll.set_child(Some(&list)); + scroll +} + +fn build_upcoming_card(note: &Note) -> gtk4::Box { + let row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .margin_start(8) + .margin_end(8) + .margin_top(4) + .margin_bottom(4) + .css_classes(["note-card"]) + .build(); + + let time_str = note + .effective_time() + .map(|t| { + let local: chrono::DateTime = t.into(); + local.format("%a %b %d, %H:%M").to_string() + }) + .unwrap_or_default(); + + let time_label = gtk4::Label::builder() + .label(&time_str) + .width_chars(18) + .xalign(0.0) + .build(); + + let body_label = gtk4::Label::builder() + .label(¬e.body) + .hexpand(true) + .xalign(0.0) + .ellipsize(gtk4::pango::EllipsizeMode::End) + .build(); + + let type_label = gtk4::Label::builder() + .label(note.note_type.as_str()) + .css_classes(["type-chip"]) + .build(); + + row.append(&time_label); + row.append(&body_label); + row.append(&type_label); + row +} diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml new file mode 100644 index 0000000..d9acf83 --- /dev/null +++ b/breadpad-shared/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "breadpad-shared" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +anyhow.workspace = true +tracing.workspace = true +serde.workspace = true +serde_json.workspace = true +uuid.workspace = true +chrono.workspace = true +rrule.workspace = true +tokio.workspace = true +zbus.workspace = true +ort.workspace = true +tokenizers.workspace = true +ndarray.workspace = true +toml.workspace = true +dirs.workspace = true +regex.workspace = true +ureq.workspace = true +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +ical = "0.11" + +[dev-dependencies] +tempfile = "3" diff --git a/breadpad-shared/src/ai.rs b/breadpad-shared/src/ai.rs new file mode 100644 index 0000000..39d16de --- /dev/null +++ b/breadpad-shared/src/ai.rs @@ -0,0 +1,118 @@ +use crate::config::OllamaConfig; +use crate::types::{ClassificationResult, NoteType}; +use serde::Deserialize; + +pub struct OllamaClient { + endpoint: String, + model: String, +} + +#[derive(Deserialize)] +struct OllamaGenerateResponse { + response: String, + #[allow(dead_code)] + done: bool, +} + +#[derive(Deserialize)] +struct OllamaClassification { + #[serde(rename = "type")] + note_type: Option, + body: Option, + confidence: Option, +} + +impl OllamaClient { + pub fn new(cfg: &OllamaConfig) -> Self { + OllamaClient { + endpoint: cfg.endpoint.trim_end_matches('/').to_string(), + model: cfg.model.clone(), + } + } + + /// Run Tier 3 classification. Returns `fallback` if Ollama is unreachable or returns + /// an unparseable response. Time, rrule, and body from Tier 1 are always preserved. + pub fn classify(&self, text: &str, fallback: &ClassificationResult) -> ClassificationResult { + match self.try_classify(text, fallback) { + Ok(r) => r, + Err(e) => { + tracing::warn!("Tier 3 (Ollama) unavailable: {}; using Tier 2 result", e); + fallback.clone() + } + } + } + + fn try_classify(&self, text: &str, fallback: &ClassificationResult) -> anyhow::Result { + let url = format!("{}/api/generate", self.endpoint); + + let prompt = format!( + "Classify the following note into exactly one type.\n\ + Valid types: todo, reminder, idea, note, question.\n\ + Note: \"{}\"\n\ + Respond with JSON only, using this exact format: \ + {{\"type\": \"TYPENAME\", \"body\": \"cleaned text\", \"confidence\": 0.0}}", + text + ); + + let payload = serde_json::json!({ + "model": self.model, + "prompt": prompt, + "format": "json", + "stream": false + }); + + let response = ureq::post(&url) + .set("Content-Type", "application/json") + .send_json(payload) + .map_err(|e| anyhow::anyhow!("Ollama HTTP error: {}", e))?; + + let ollama_resp: OllamaGenerateResponse = response + .into_json() + .map_err(|e| anyhow::anyhow!("deserialize Ollama envelope: {}", e))?; + + let classification: OllamaClassification = serde_json::from_str(&ollama_resp.response) + .map_err(|e| anyhow::anyhow!( + "parse Ollama classification JSON: {} — raw: {:?}", + e, + &ollama_resp.response + ))?; + + let note_type = classification + .note_type + .as_deref() + .map(NoteType::from_str) + .unwrap_or_else(|| fallback.note_type.clone()); + + let confidence = classification + .confidence + .unwrap_or(0.75) + .clamp(0.0, 1.0); + + // Use Tier 1's time/rrule/body as the ground truth; optionally use the LLM's + // cleaned body if it provided one and Tier 1 didn't already strip anything. + let body = if let Some(llm_body) = classification.body.filter(|b| !b.trim().is_empty()) { + if fallback.body == text { + // Tier 1 didn't clean the body — accept LLM's version + llm_body + } else { + // Tier 1 already stripped time phrases — keep its result + fallback.body.clone() + } + } else { + fallback.body.clone() + }; + + tracing::info!( + "Tier 3: classified {:?} as {:?} (conf={:.2})", + text, note_type, confidence + ); + + Ok(ClassificationResult { + note_type, + confidence, + time: fallback.time, + rrule: fallback.rrule.clone(), + body, + }) + } +} diff --git a/breadpad-shared/src/calendar.rs b/breadpad-shared/src/calendar.rs new file mode 100644 index 0000000..56f9cd1 --- /dev/null +++ b/breadpad-shared/src/calendar.rs @@ -0,0 +1,221 @@ +use crate::config::CalendarConfig; +use crate::types::Note; +use anyhow::{Context, Result}; +use ical::IcalParser; +use std::io::BufReader; + +pub struct CalDavClient { + config: CalendarConfig, + client: reqwest::Client, +} + +pub struct CalDavEventInfo { + pub uid: String, + pub summary: String, +} + +impl CalDavClient { + pub fn new(config: CalendarConfig) -> Self { + let client = reqwest::Client::builder() + .build() + .expect("failed to build HTTP client"); + CalDavClient { config, client } + } + + pub async fn test_connection(&self) -> Result<()> { + let body = r#""#; + let resp = self + .client + .request( + reqwest::Method::from_bytes(b"PROPFIND").unwrap(), + &self.config.url, + ) + .basic_auth(&self.config.username, Some(&self.config.password)) + .header("Depth", "0") + .header("Content-Type", "application/xml; charset=utf-8") + .body(body) + .send() + .await + .context("CalDAV PROPFIND request failed")?; + + let status = resp.status(); + if status.is_success() || status.as_u16() == 207 { + Ok(()) + } else { + anyhow::bail!("CalDAV server returned {}", status); + } + } + + pub async fn push_event(&self, note: &Note) -> Result { + let uid = caldav_uid(note); + let ical = build_ical(note, &uid); + let url = event_url(&self.config.url, &uid); + + let resp = self + .client + .put(&url) + .basic_auth(&self.config.username, Some(&self.config.password)) + .header("Content-Type", "text/calendar; charset=utf-8") + .body(ical) + .send() + .await + .context("CalDAV PUT request failed")?; + + let status = resp.status(); + if status.is_success() || status.as_u16() == 201 || status.as_u16() == 204 { + Ok(uid) + } else { + anyhow::bail!("CalDAV PUT returned {}", status); + } + } + + pub async fn delete_event(&self, uid: &str) -> Result<()> { + let url = event_url(&self.config.url, uid); + + let resp = self + .client + .delete(&url) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send() + .await + .context("CalDAV DELETE request failed")?; + + let status = resp.status(); + if status.is_success() || status.as_u16() == 204 || status.as_u16() == 404 { + Ok(()) + } else { + anyhow::bail!("CalDAV DELETE returned {}", status); + } + } + + pub async fn list_events(&self) -> Result> { + let body = r#" + + + + + + + + + + +"#; + + let resp = self + .client + .request( + reqwest::Method::from_bytes(b"REPORT").unwrap(), + &self.config.url, + ) + .basic_auth(&self.config.username, Some(&self.config.password)) + .header("Depth", "1") + .header("Content-Type", "application/xml; charset=utf-8") + .body(body) + .send() + .await + .context("CalDAV REPORT request failed")?; + + let status = resp.status(); + if !status.is_success() && status.as_u16() != 207 { + anyhow::bail!("CalDAV REPORT returned {}", status); + } + + let xml = resp.text().await.context("failed to read CalDAV REPORT body")?; + parse_report_response(&xml) + } +} + +pub fn caldav_uid(note: &Note) -> String { + note.caldav_uid + .clone() + .unwrap_or_else(|| format!("{}@breadpad", note.id)) +} + +fn event_url(base: &str, uid: &str) -> String { + format!("{}/{}.ics", base.trim_end_matches('/'), uid) +} + +fn build_ical(note: &Note, uid: &str) -> String { + let dt = note.time.unwrap_or(note.created); + let dtstart = dt.format("%Y%m%dT%H%M%SZ").to_string(); + let summary = escape_ical(¬e.body); + let description = escape_ical(&format!("type={}", note.note_type.as_str())); + + let mut ical = format!( + "BEGIN:VCALENDAR\r\n\ + VERSION:2.0\r\n\ + PRODID:-//breadpad//EN\r\n\ + BEGIN:VEVENT\r\n\ + UID:{uid}\r\n\ + SUMMARY:{summary}\r\n\ + DTSTART:{dtstart}\r\n\ + DTEND:{dtstart}\r\n\ + DESCRIPTION:{description}\r\n" + ); + + if let Some(rrule) = ¬e.rrule { + ical.push_str(rrule.as_str()); + ical.push_str("\r\n"); + } + + ical.push_str("END:VEVENT\r\nEND:VCALENDAR\r\n"); + ical +} + +fn escape_ical(s: &str) -> String { + s.replace('\\', "\\\\") + .replace(';', "\\;") + .replace(',', "\\,") + .replace('\n', "\\n") +} + +fn parse_report_response(xml: &str) -> Result> { + let mut events = Vec::new(); + let mut search_from = 0; + + while let Some(start) = xml[search_from..].find("BEGIN:VCALENDAR") { + let abs_start = search_from + start; + let tail = &xml[abs_start..]; + let end = match tail.find("END:VCALENDAR") { + Some(e) => abs_start + e + "END:VCALENDAR".len(), + None => break, + }; + let ical_block = &xml[abs_start..end]; + events.extend(parse_ical_block(ical_block)); + search_from = end; + } + + Ok(events) +} + +fn parse_ical_block(data: &str) -> Vec { + let reader = BufReader::new(data.as_bytes()); + let parser = IcalParser::new(reader); + let mut out = Vec::new(); + + for item in parser { + match item { + Ok(cal) => { + for event in cal.events { + let uid = event + .properties + .iter() + .find(|p| p.name == "UID") + .and_then(|p| p.value.clone()) + .unwrap_or_default(); + let summary = event + .properties + .iter() + .find(|p| p.name == "SUMMARY") + .and_then(|p| p.value.clone()) + .unwrap_or_default(); + out.push(CalDavEventInfo { uid, summary }); + } + } + Err(e) => tracing::warn!("CalDAV: failed to parse VCALENDAR block: {}", e), + } + } + + out +} diff --git a/breadpad-shared/src/classifier.rs b/breadpad-shared/src/classifier.rs new file mode 100644 index 0000000..adf1369 --- /dev/null +++ b/breadpad-shared/src/classifier.rs @@ -0,0 +1,284 @@ +use crate::ai::OllamaClient; +use crate::config::OllamaConfig; +use crate::parser::parse_rule_based; +use crate::types::{ClassificationResult, NoteType}; +use std::path::PathBuf; + +/// Minimum Tier 1 confidence needed to skip Tier 2 entirely. +const TIER1_SKIP_THRESHOLD: f32 = 0.82; + +#[derive(Debug, Clone, PartialEq)] +pub enum ExecutionProvider { + Qnn, + Vulkan, + Cpu, +} + +impl ExecutionProvider { + pub fn as_str(&self) -> &str { + match self { + ExecutionProvider::Qnn => "QNN (NPU)", + ExecutionProvider::Vulkan => "Vulkan", + ExecutionProvider::Cpu => "CPU", + } + } +} + +pub struct Classifier { + session: Option, + tokenizer: Option, + pub active_provider: ExecutionProvider, + pub model_path: PathBuf, + pub default_morning: String, + ollama: Option, +} + +fn model_dir() -> PathBuf { + dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from("~/.local/share")) + .join("breadpad") + .join("model") +} + +impl Classifier { + /// Load with Tier 1 + optional Tier 2 (ONNX). Tier 3 disabled unless + /// `.with_ollama()` is called on the returned value. + pub fn load(ep_pref: &str, default_morning: &str) -> Self { + let dir = model_dir(); + let onnx_path = dir.join("classifier.onnx"); + let tok_path = dir.join("tokenizer.json"); + + let (session, active_provider) = if onnx_path.exists() { + try_load_session(&onnx_path, ep_pref) + } else { + tracing::warn!("model not found at {:?}; Tier 2 disabled", onnx_path); + (None, ExecutionProvider::Cpu) + }; + + let tokenizer = if tok_path.exists() && session.is_some() { + match tokenizers::Tokenizer::from_file(&tok_path) { + Ok(tok) => Some(tok), + Err(e) => { + tracing::warn!("failed to load tokenizer: {}", e); + None + } + } + } else { + None + }; + + Classifier { + session, + tokenizer, + active_provider, + model_path: onnx_path, + default_morning: default_morning.to_string(), + ollama: None, + } + } + + /// Enable Tier 3 (Ollama). Only has an effect if `cfg.enabled` is true. + pub fn with_ollama(mut self, cfg: OllamaConfig) -> Self { + self.ollama = if cfg.enabled { Some(cfg) } else { None }; + self + } + + /// Three-tier classification pipeline: + /// + /// - **Tier 1** (rule-based parser): always runs; handles time/recurrence extraction + /// and obvious type signals. If confidence ≥ 0.82, result is returned immediately. + /// - **Tier 2** (small ONNX model): runs when Tier 1 is uncertain about the type. + /// Responsible for type classification only; Tier 1's time/rrule/body are preserved. + /// - **Tier 3** (Ollama LLM): runs when Tier 2 confidence is below the configured + /// threshold. Falls back to the Tier 2 result if Ollama is unreachable. + pub fn classify(&mut self, text: &str) -> ClassificationResult { + // ── Tier 1 ─────────────────────────────────────────────────────────── + let tier1 = parse_rule_based(text, &self.default_morning); + tracing::debug!("Tier 1: {:?} conf={:.2}", tier1.note_type, tier1.confidence); + + if tier1.confidence >= TIER1_SKIP_THRESHOLD { + return tier1; + } + + // ── Tier 2 ─────────────────────────────────────────────────────────── + // ONNX model classifies the type only; Tier 1's time/rrule/body are kept. + let tier2 = if let (Some(session), Some(tokenizer)) = + (&mut self.session, &self.tokenizer) + { + match run_onnx(session, tokenizer, text) { + Ok(r) => { + tracing::debug!("Tier 2: {:?} conf={:.2}", r.note_type, r.confidence); + ClassificationResult { + note_type: r.note_type, + confidence: r.confidence, + time: tier1.time, + rrule: tier1.rrule.clone(), + body: tier1.body.clone(), + } + } + Err(e) => { + tracing::warn!("Tier 2 inference failed: {}; using Tier 1 result", e); + tier1.clone() + } + } + } else { + tier1.clone() + }; + + // ── Tier 3 ─────────────────────────────────────────────────────────── + if let Some(ollama_cfg) = &self.ollama { + if tier2.confidence < ollama_cfg.confidence_threshold { + tracing::debug!( + "Tier 2 confidence {:.2} < threshold {:.2}; trying Tier 3", + tier2.confidence, + ollama_cfg.confidence_threshold + ); + let client = OllamaClient::new(ollama_cfg); + return client.classify(text, &tier2); + } + } + + tier2 + } + + pub fn model_available(&self) -> bool { + self.session.is_some() + } +} + +// NLI hypotheses paired with their note types. The model scores each as +// entailment (label 0) vs not_entailment (label 1); we pick the highest +// entailment score across all five passes. +const HYPOTHESES: [(&str, &str); 5] = [ + ("This note is a task or action item to complete.", "todo"), + ("This note is a reminder with a specific time or deadline.", "reminder"), + ("This note is an idea, suggestion, or creative thought.", "idea"), + ("This note is a general observation or piece of information.", "note"), + ("This note is a question that needs an answer.", "question"), +]; + +fn run_onnx( + session: &mut ort::session::Session, + tokenizer: &tokenizers::Tokenizer, + text: &str, +) -> anyhow::Result { + const ENTAILMENT_IDX: usize = 0; + + let mut entailment_scores = [0.0f32; 5]; + + for (i, (hypothesis, _)) in HYPOTHESES.iter().enumerate() { + let encoding = tokenizer + .encode((text, *hypothesis), true) + .map_err(|e| anyhow::anyhow!("tokenize: {}", e))?; + + let ids: Vec = encoding.get_ids().iter().map(|&x| x as i64).collect(); + let mask: Vec = encoding.get_attention_mask().iter().map(|&x| x as i64).collect(); + let len = ids.len(); + + let ids_tensor = ort::value::Tensor::::from_array( + (vec![1i64, len as i64], ids) + ).map_err(|e| anyhow::anyhow!("ids tensor: {}", e))?; + let mask_tensor = ort::value::Tensor::::from_array( + (vec![1i64, len as i64], mask) + ).map_err(|e| anyhow::anyhow!("mask tensor: {}", e))?; + + let inputs = ort::inputs![ + "input_ids" => ids_tensor, + "attention_mask" => mask_tensor, + ]; + let outputs = session + .run(inputs) + .map_err(|e| anyhow::anyhow!("run: {}", e))?; + + let logits = outputs["logits"] + .try_extract_tensor::() + .map_err(|e| anyhow::anyhow!("extract logits: {}", e))?; + let (_, logits_slice) = logits; + + entailment_scores[i] = logits_slice + .get(ENTAILMENT_IDX) + .copied() + .unwrap_or(0.0); + } + + let best_idx = entailment_scores + .iter() + .enumerate() + .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) + .map(|(i, _)| i) + .unwrap_or(3); + + let note_type = NoteType::from_str(HYPOTHESES[best_idx].1); + let confidence = softmax_single(&entailment_scores, best_idx); + + Ok(ClassificationResult { + note_type, + confidence, + // Time/rrule/body are merged by the caller from Tier 1's result. + time: None, + rrule: None, + body: text.to_string(), + }) +} + +fn softmax_single(logits: &[f32], idx: usize) -> f32 { + if logits.is_empty() || idx >= logits.len() { + return 0.5; + } + let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max); + let exps: Vec = logits.iter().map(|&x| (x - max).exp()).collect(); + let sum: f32 = exps.iter().sum(); + exps[idx] / sum +} + +fn try_load_session( + path: &std::path::Path, + ep_pref: &str, +) -> (Option, ExecutionProvider) { + let providers: &[(&str, ExecutionProvider)] = &[ + ("qnn", ExecutionProvider::Qnn), + ("vulkan", ExecutionProvider::Vulkan), + ("cpu", ExecutionProvider::Cpu), + ]; + + let to_try: Vec<&(&str, ExecutionProvider)> = match ep_pref { + "npu" => providers[..1].iter().collect(), + "vulkan" => providers[1..2].iter().collect(), + "cpu" => providers[2..].iter().collect(), + _ => providers.iter().collect(), + }; + + for (ep_name, ep) in to_try { + match build_session(path, ep_name) { + Ok(session) => { + tracing::info!("ONNX session loaded with {} EP", ep.as_str()); + return (Some(session), ep.clone()); + } + Err(e) => { + tracing::debug!("{} EP unavailable: {}", ep_name, e); + } + } + } + + (None, ExecutionProvider::Cpu) +} + +fn build_session( + path: &std::path::Path, + ep_name: &str, +) -> anyhow::Result { + match ep_name { + "cpu" => { + let builder = ort::session::Session::builder() + .map_err(|e| anyhow::anyhow!("builder: {}", e))?; + let mut builder = builder + .with_execution_providers([ort::ep::CPU::default().build()]) + .map_err(|e| anyhow::anyhow!("ep: {}", e))?; + let session = builder + .commit_from_file(path) + .map_err(|e| anyhow::anyhow!("load: {}", e))?; + Ok(session) + } + _ => Err(anyhow::anyhow!("EP '{}' not available in this build", ep_name)), + } +} diff --git a/breadpad-shared/src/config.rs b/breadpad-shared/src/config.rs new file mode 100644 index 0000000..f65b392 --- /dev/null +++ b/breadpad-shared/src/config.rs @@ -0,0 +1,180 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +fn default_type_str() -> String { "note".into() } +fn default_workspace_tag() -> bool { true } +fn default_snooze_options() -> Vec { + vec!["15m".into(), "1h".into(), "tomorrow_morning".into()] +} +fn default_archive_after_days() -> i64 { 30 } +fn default_model_path() -> String { "~/.local/share/breadpad/model/classifier.onnx".into() } +fn default_tokenizer_path() -> String { "~/.local/share/breadpad/model/tokenizer.json".into() } +fn default_execution_provider() -> String { "auto".into() } +fn default_morning_time() -> String { "08:00".into() } +fn default_missed_grace_minutes() -> i64 { 60 } +fn default_ollama_endpoint() -> String { "http://localhost:11434".into() } +fn default_ollama_model() -> String { "llama3.2:3b".into() } +fn default_ollama_confidence_threshold() -> f32 { 0.6 } +fn default_ollama_enabled() -> bool { true } +fn default_calendar_enabled() -> bool { false } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Settings { + #[serde(default = "default_type_str")] + pub default_type: String, + #[serde(default = "default_workspace_tag")] + pub workspace_tag: bool, + #[serde(default = "default_snooze_options")] + pub snooze_options: Vec, + #[serde(default = "default_archive_after_days")] + pub archive_after_days: i64, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + default_type: default_type_str(), + workspace_tag: default_workspace_tag(), + snooze_options: default_snooze_options(), + archive_after_days: default_archive_after_days(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OllamaConfig { + #[serde(default = "default_ollama_endpoint")] + pub endpoint: String, + #[serde(default = "default_ollama_model")] + pub model: String, + #[serde(default = "default_ollama_confidence_threshold")] + pub confidence_threshold: f32, + #[serde(default = "default_ollama_enabled")] + pub enabled: bool, +} + +impl Default for OllamaConfig { + fn default() -> Self { + OllamaConfig { + endpoint: default_ollama_endpoint(), + model: default_ollama_model(), + confidence_threshold: default_ollama_confidence_threshold(), + enabled: default_ollama_enabled(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelConfig { + #[serde(default = "default_model_path")] + pub path: String, + #[serde(default = "default_tokenizer_path")] + pub tokenizer: String, + #[serde(default = "default_execution_provider")] + pub execution_provider: String, + #[serde(default)] + pub ollama: OllamaConfig, +} + +impl Default for ModelConfig { + fn default() -> Self { + ModelConfig { + path: default_model_path(), + tokenizer: default_tokenizer_path(), + execution_provider: default_execution_provider(), + ollama: OllamaConfig::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RemindersConfig { + #[serde(default = "default_morning_time")] + pub default_morning: String, + #[serde(default = "default_missed_grace_minutes")] + pub missed_grace_minutes: i64, +} + +impl Default for RemindersConfig { + fn default() -> Self { + RemindersConfig { + default_morning: default_morning_time(), + missed_grace_minutes: default_missed_grace_minutes(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CalendarConfig { + #[serde(default = "default_calendar_enabled")] + pub enabled: bool, + #[serde(default)] + pub url: String, + #[serde(default)] + pub username: String, + #[serde(default)] + pub password: String, +} + +impl Default for CalendarConfig { + fn default() -> Self { + CalendarConfig { + enabled: false, + url: String::new(), + username: String::new(), + password: String::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Config { + #[serde(default)] + pub settings: Settings, + #[serde(default)] + pub model: ModelConfig, + #[serde(default)] + pub reminders: RemindersConfig, + #[serde(default)] + pub calendar: CalendarConfig, +} + +impl Config { + pub fn load() -> Result { + let path = config_path(); + if !path.exists() { + let cfg = Config::default(); + cfg.save()?; + return Ok(cfg); + } + let text = fs::read_to_string(&path)?; + let cfg: Config = toml::from_str(&text)?; + Ok(cfg) + } + + pub fn save(&self) -> Result<()> { + let path = config_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let text = toml::to_string_pretty(self)?; + fs::write(&path, text)?; + Ok(()) + } +} + +pub fn config_path() -> PathBuf { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from("~/.config")) + .join("breadpad") + .join("breadpad.toml") +} + +pub fn style_css_path() -> PathBuf { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from("~/.config")) + .join("breadpad") + .join("style.css") +} diff --git a/breadpad-shared/src/lib.rs b/breadpad-shared/src/lib.rs new file mode 100644 index 0000000..1fa87d5 --- /dev/null +++ b/breadpad-shared/src/lib.rs @@ -0,0 +1,9 @@ +pub mod ai; +pub mod calendar; +pub mod classifier; +pub mod config; +pub mod parser; +pub mod scheduler; +pub mod store; +pub mod theme; +pub mod types; diff --git a/breadpad-shared/src/parser.rs b/breadpad-shared/src/parser.rs new file mode 100644 index 0000000..c298429 --- /dev/null +++ b/breadpad-shared/src/parser.rs @@ -0,0 +1,882 @@ +use crate::types::{ClassificationResult, NoteType, RecurrenceRule}; +use chrono::{DateTime, Datelike, Duration, Local, NaiveTime, Timelike, Utc, Weekday}; +use regex::Regex; +use std::sync::OnceLock; + +struct Patterns { + at_time: Regex, + in_duration: Regex, + in_duration_word: Regex, + tomorrow: Regex, + next_weekday: Regex, + tonight: Regex, + every_weekdays: Regex, + every_weekday: Regex, + every_week: Regex, + every_day: Regex, + morning_evening: Regex, +} + +static PATTERNS: OnceLock = OnceLock::new(); + +fn patterns() -> &'static Patterns { + PATTERNS.get_or_init(|| Patterns { + at_time: Regex::new(r"(?i)\bat\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?").unwrap(), + in_duration: Regex::new(r"(?i)\bin\s+(\d+)\s+(minute|hour|day)s?").unwrap(), + // Word-form durations: "in an hour", "in a couple of hours", "in half an hour" + in_duration_word: Regex::new( + r"(?i)\bin\s+(?:an?\s+hour|a\s+couple\s+of\s+hours?|a\s+few\s+hours?|half\s+an?\s+hour|an?\s+minutes?|a\s+couple\s+of\s+minutes?)" + ).unwrap(), + tomorrow: Regex::new(r"(?i)\btomorrow(?:\s+morning|\s+evening|\s+afternoon)?").unwrap(), + next_weekday: Regex::new(r"(?i)\bnext\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)").unwrap(), + // "tonight" or "this evening" — maps to a fixed 21:00 anchor + tonight: Regex::new(r"(?i)\b(?:tonight|this\s+evening)\b").unwrap(), + // "every weekday [at H:MM|morning|afternoon|evening]" → Mon–Fri RRULE + every_weekdays: Regex::new( + r"(?i)\bevery\s+weekday(?:\s+(?:at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?|(morning|afternoon|evening)))?" + ).unwrap(), + every_weekday: Regex::new(r"(?i)\bevery\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)(?:\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?").unwrap(), + // \bweek\b prevents "weekday" from being matched here + every_week: Regex::new(r"(?i)\bevery\s+week\b(?:\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?").unwrap(), + every_day: Regex::new(r"(?i)\bevery\s+day(?:\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?").unwrap(), + // Strips stray time-of-day words after a time has been extracted. + // Handles compound forms ("this morning") before bare words to avoid partial matches. + morning_evening: Regex::new( + r"(?i)\b(?:this\s+(?:morning|evening|afternoon|night)|tonight|morning|evening|afternoon|night)\b" + ).unwrap(), + }) +} + +fn parse_time_of_day(hour: &str, min: Option<&str>, ampm: Option<&str>) -> NaiveTime { + let mut h: u32 = hour.parse().unwrap_or(9); + let m: u32 = min.and_then(|s| s.parse().ok()).unwrap_or(0); + if let Some(ap) = ampm { + if ap.eq_ignore_ascii_case("pm") && h < 12 { + h += 12; + } else if ap.eq_ignore_ascii_case("am") && h == 12 { + h = 0; + } + } + NaiveTime::from_hms_opt(h, m, 0).unwrap_or(NaiveTime::from_hms_opt(9, 0, 0).unwrap()) +} + +fn weekday_from_str(s: &str) -> Weekday { + match s.to_lowercase().as_str() { + "monday" => Weekday::Mon, + "tuesday" => Weekday::Tue, + "wednesday" => Weekday::Wed, + "thursday" => Weekday::Thu, + "friday" => Weekday::Fri, + "saturday" => Weekday::Sat, + _ => Weekday::Sun, + } +} + +fn rrule_weekday(wd: Weekday) -> &'static str { + match wd { + Weekday::Mon => "MO", + Weekday::Tue => "TU", + Weekday::Wed => "WE", + Weekday::Thu => "TH", + Weekday::Fri => "FR", + Weekday::Sat => "SA", + Weekday::Sun => "SU", + } +} + +fn next_occurrence_of_weekday(wd: Weekday, time: NaiveTime) -> DateTime { + let local = Local::now(); + let days_ahead = (wd.num_days_from_monday() as i64 + - local.weekday().num_days_from_monday() as i64) + .rem_euclid(7); + let days_ahead = if days_ahead == 0 { + if local.time() < time { + 0 + } else { + 7 + } + } else { + days_ahead + }; + let target_date = local.date_naive() + Duration::days(days_ahead); + let naive = target_date.and_time(time); + naive.and_local_timezone(Local).unwrap().with_timezone(&Utc) +} + +pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResult { + let p = patterns(); + let morning_time: NaiveTime = default_morning + .split(':') + .collect::>() + .as_slice() + .get(..2) + .and_then(|parts| { + let h: u32 = parts[0].parse().ok()?; + let m: u32 = parts[1].parse().ok()?; + NaiveTime::from_hms_opt(h, m, 0) + }) + .unwrap_or(NaiveTime::from_hms_opt(8, 0, 0).unwrap()); + + let evening_time = NaiveTime::from_hms_opt(18, 0, 0).unwrap(); + + let mut extracted_time: Option> = None; + let mut rrule: Option = None; + let mut cleaned = text.to_string(); + + // Recurrence: every day + if let Some(m) = p.every_day.find(text) { + let caps = p.every_day.captures(text).unwrap(); + let t = if let (Some(h), mp, ap) = (caps.get(1), caps.get(2), caps.get(3)) { + parse_time_of_day(h.as_str(), mp.map(|x| x.as_str()), ap.map(|x| x.as_str())) + } else { + morning_time + }; + rrule = Some(RecurrenceRule::new(format!( + "RRULE:FREQ=DAILY;BYHOUR={};BYMINUTE={};BYSECOND=0", + t.hour(), + t.minute() + ))); + cleaned = cleaned.replacen(m.as_str(), "", 1).trim().to_string(); + } + // Recurrence: every week + else if let Some(m) = p.every_week.find(text) { + let caps = p.every_week.captures(text).unwrap(); + let t = if let (Some(h), mp, ap) = (caps.get(1), caps.get(2), caps.get(3)) { + parse_time_of_day(h.as_str(), mp.map(|x| x.as_str()), ap.map(|x| x.as_str())) + } else { + morning_time + }; + let now = Local::now(); + let wd = rrule_weekday(now.weekday()); + rrule = Some(RecurrenceRule::new(format!( + "RRULE:FREQ=WEEKLY;BYDAY={};BYHOUR={};BYMINUTE={};BYSECOND=0", + wd, + t.hour(), + t.minute() + ))); + cleaned = cleaned.replacen(m.as_str(), "", 1).trim().to_string(); + } + // Recurrence: every weekday (Mon–Fri) + else if let Some(caps) = p.every_weekdays.captures(text) { + let t = if let Some(h) = caps.get(1) { + parse_time_of_day(h.as_str(), caps.get(2).map(|x| x.as_str()), caps.get(3).map(|x| x.as_str())) + } else { + match caps.get(4).map(|x| x.as_str().to_lowercase()).as_deref() { + Some("afternoon") => NaiveTime::from_hms_opt(14, 0, 0).unwrap(), + Some("evening") => evening_time, + _ => morning_time, + } + }; + rrule = Some(RecurrenceRule::new(format!( + "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR={};BYMINUTE={};BYSECOND=0", + t.hour(), + t.minute() + ))); + let full_match = caps.get(0).unwrap().as_str(); + cleaned = cleaned.replacen(full_match, "", 1).trim().to_string(); + } + // Recurrence: every + else if let Some(caps) = p.every_weekday.captures(text) { + let wd_str = caps.get(1).unwrap().as_str(); + let wd = weekday_from_str(wd_str); + let t = if let (Some(h), mp, ap) = (caps.get(2), caps.get(3), caps.get(4)) { + parse_time_of_day(h.as_str(), mp.map(|x| x.as_str()), ap.map(|x| x.as_str())) + } else { + morning_time + }; + rrule = Some(RecurrenceRule::new(format!( + "RRULE:FREQ=WEEKLY;BYDAY={};BYHOUR={};BYMINUTE={};BYSECOND=0", + rrule_weekday(wd), + t.hour(), + t.minute() + ))); + extracted_time = Some(next_occurrence_of_weekday(wd, t)); + let full_match = caps.get(0).unwrap().as_str(); + cleaned = cleaned.replacen(full_match, "", 1).trim().to_string(); + } + + // One-off: at