From 52ea57057e5def74d00caefec50751586a5058a1 Mon Sep 17 00:00:00 2001 From: ranfdev Date: Sun, 8 Oct 2023 15:57:09 +0200 Subject: [PATCH] init notify --- Cargo.lock | 3165 ++++++++++++++++ Cargo.toml | 20 +- README.md | 76 +- build-aux/com.ranfdev.Notify.Devel.json | 18 +- data/icons/com.ranfdev.Notify-symbolic.svg | 65 +- data/icons/com.ranfdev.Notify.Devel.svg | 218 +- data/icons/com.ranfdev.Notify.Source.svg | 3172 +++++++++++++++++ data/icons/com.ranfdev.Notify.svg | 80 +- data/icons/dice3-symbolic.svg | 2 + data/resources/dice3-symbolic.svg | 2 + data/resources/meson.build | 1 + data/resources/resources.gresource.xml | 5 + data/resources/style.css | 27 + .../resources/ui/subscription_info_dialog.blp | 50 + data/resources/ui/window.blp | 168 +- ntfy-daemon/.gitignore | 1 + ntfy-daemon/Cargo.lock | 2312 ++++++++++++ ntfy-daemon/Cargo.toml | 31 + ntfy-daemon/README.md | 5 + ntfy-daemon/build.rs | 6 + ntfy-daemon/data/mailer_emoji_map.json | 1857 ++++++++++ ntfy-daemon/src/lib.rs | 24 + .../src/message_repo/migrations/00.sql | 29 + ntfy-daemon/src/message_repo/mod.rs | 206 ++ ntfy-daemon/src/models.rs | 263 ++ ntfy-daemon/src/ntfy.capnp | 47 + ntfy-daemon/src/ntfy_proxy.rs | 355 ++ ntfy-daemon/src/retry.rs | 56 + ntfy-daemon/src/system_client.rs | 504 +++ rustfmt.toml | 2 + src/application.rs | 166 +- src/async_utils.rs | 28 + src/main.rs | 8 +- src/subscription.rs | 318 ++ src/widgets/add_subscription_dialog.rs | 180 + src/widgets/message_row.rs | 209 ++ src/widgets/mod.rs | 8 + src/widgets/subscription_info_dialog.rs | 112 + src/widgets/window.rs | 483 +++ src/window.rs | 118 - 40 files changed, 13845 insertions(+), 552 deletions(-) create mode 100644 Cargo.lock create mode 100644 data/icons/com.ranfdev.Notify.Source.svg create mode 100644 data/icons/dice3-symbolic.svg create mode 100644 data/resources/dice3-symbolic.svg create mode 100644 data/resources/ui/subscription_info_dialog.blp create mode 100644 ntfy-daemon/.gitignore create mode 100644 ntfy-daemon/Cargo.lock create mode 100644 ntfy-daemon/Cargo.toml create mode 100644 ntfy-daemon/README.md create mode 100644 ntfy-daemon/build.rs create mode 100644 ntfy-daemon/data/mailer_emoji_map.json create mode 100644 ntfy-daemon/src/lib.rs create mode 100644 ntfy-daemon/src/message_repo/migrations/00.sql create mode 100644 ntfy-daemon/src/message_repo/mod.rs create mode 100644 ntfy-daemon/src/models.rs create mode 100644 ntfy-daemon/src/ntfy.capnp create mode 100644 ntfy-daemon/src/ntfy_proxy.rs create mode 100644 ntfy-daemon/src/retry.rs create mode 100644 ntfy-daemon/src/system_client.rs create mode 100644 rustfmt.toml create mode 100644 src/async_utils.rs create mode 100644 src/subscription.rs create mode 100644 src/widgets/add_subscription_dialog.rs create mode 100644 src/widgets/message_row.rs create mode 100644 src/widgets/mod.rs create mode 100644 src/widgets/subscription_info_dialog.rs create mode 100644 src/widgets/window.rs delete mode 100644 src/window.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0e3f7f5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3165 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" + +[[package]] +name = "ashpd" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3affe251686bd936a0afb74b9693e8bf2f193d51da1b9a45d3f1303a9bd2cc7" +dependencies = [ + "async-std", + "enumflags2", + "futures-channel", + "futures-util", + "once_cell", + "rand", + "serde", + "serde_repr", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 1.9.0", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.23", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.37.23", + "signal-hook", + "windows-sys", +] + +[[package]] +name = "async-recursion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[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.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand 1.9.0", + "futures-lite", + "log", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cairo-rs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d859b656775a6b1dd078d3e5924884e6ea88aa649a7fdde03d5b2ec56ffcc10b" +dependencies = [ + "bitflags 2.3.3", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd4d115132e01c0165e3bf5f56aedee8980b0b96ede4eb000b693c05a8adb8ff" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "capnp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e65021d89250bbfe7c2791789ced2c4bdc21b0e8bb59c64f3fd6145a5fd678" + +[[package]] +name = "capnp-futures" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d520e0af228b92de357f230f4987ee4f9786f2b8aa24b9cfe53f5b11c17198" +dependencies = [ + "capnp", + "futures", +] + +[[package]] +name = "capnp-rpc" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab8e869783e491cbcc350427a5e775aa4d8a1deaa5198d74332957cfa430779" +dependencies = [ + "capnp", + "capnp-futures", + "futures", +] + +[[package]] +name = "capnpc" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbc3763fb3e6635188e9cc51ee11a26f8777c553ca377430818dbebaaf6042b" +dependencies = [ + "capnp", +] + +[[package]] +name = "cc" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-expr" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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 = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset 0.9.0", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +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 = "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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc9c2ed73a81d556b65d08879ba4ee58808a6b1927ce915262185d6d547c6f3" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8965ed5455cbfa1eb45c14a3b971cbacb43481913a3a5b9078b2ef0d9a01bd4b" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeffd4a7d68046c28666d817071bf025254aaed4df35099443f0c306ca6177f3" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generational-arena" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877e94aff08e743b651baaea359664321055749b398adff8740a7399af7796e7" +dependencies = [ + "cfg-if", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gettext-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" +dependencies = [ + "cc", + "temp-dir", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "gio" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7884cba6b1c5db1607d970cadf44b14a43913d42bc68766eea6a5e2fe0891524" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331156127e8166dd815cf8d2db3a5beb492610c716c03ee6db4f2d07092af0a7" +dependencies = [ + "bitflags 2.3.3", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "179643c50bf28d20d2f6eacd2531a88f2f5d9747dd0b86b8af1e8bb5dd0de3c0" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2228cda1505613a7a956cca69076892cfbda84fc2b7a62b94a41a272c0c401" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4144cee8fc8788f2a9b73dc5f1d4e1189d1f95305c4cb7bd9c1af1cfa31f59" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b3b9e8090acb325d08cde84b30fc29b963a75fbac93160e9c5a30d2b335742" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0770d1ede7189092748c4ef5b6921264eb945fd2318a58e8f59dc273000810f8" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0973f9b9ade10fc22403810b7283d47b533033715f2aaa36288cd3bf90d63efc" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9f2c4530f3168fe506b0be7bc15f7a93ef38d020edb27d81a921a26cbca851" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "gtk4-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fde71ef2e78ac8fb18073c345b5f29609048d4045a345613645aa1163243c1" +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 = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[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 = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.6", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libadwaita" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06444f4ca05a60693da6e9e2b591bd40a298e65a118a8d5e830771718b3e0253" +dependencies = [ + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "gtk4", + "libadwaita-sys", + "libc", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021cfe3d1fcfa82411765a791f7e9b32f35dd98ce88d2e3fa10e7320f5cc8ce7" +dependencies = [ + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +dependencies = [ + "value-bag", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "static_assertions", +] + +[[package]] +name = "notify" +version = "0.1.0" +dependencies = [ + "anyhow", + "ashpd", + "capnp", + "capnp-rpc", + "chrono", + "futures", + "gettext-rs", + "gtk4", + "libadwaita", + "ntfy-daemon", + "once_cell", + "rand", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", + "ureq", +] + +[[package]] +name = "ntfy-daemon" +version = "0.1.0" +dependencies = [ + "anyhow", + "ashpd", + "capnp", + "capnp-rpc", + "capnpc", + "clap", + "futures", + "generational-arena", + "rand", + "regex", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "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 2.0.28", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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 = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a9e54b831d033206160096b825f2070cf5fda7e35167b1c01e9e774f9202d1" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.22.6", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.3.3", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee020b1716f0a80e2ace9b03441a749e402e86712f15f16fe8a8f75afac732f" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.5", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.21.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.2", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513722fd73ad80a71f72b61009ea1b584bcfa1483ca93949c8f290298837fa59" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_json" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" + +[[package]] +name = "temp-dir" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" + +[[package]] +name = "tempfile" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +dependencies = [ + "cfg-if", + "fastrand 2.0.0", + "redox_syscall", + "rustix 0.38.6", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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 = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-webpki 0.100.1", + "url", + "webpki-roots 0.23.1", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" + +[[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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[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.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" +dependencies = [ + "rustls-webpki 0.100.1", +] + +[[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" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46aab759304e4d7b2075a9aecba26228bb073ee8c50db796b2c72c676b5d807" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "zbus" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zvariant" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml index 8a3f4bd..494421c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,26 @@ edition = "2021" [profile.release] lto = true +[workspace] +members = [ + "ntfy-daemon" +] + [dependencies] +ntfy-daemon = { path = "./ntfy-daemon" } gettext-rs = { version = "0.7", features = ["gettext-system"] } -gtk = { version = "0.6", package = "gtk4", features = ["v4_8"] } +gtk = { version = "0.7", package = "gtk4", features = ["v4_12", "blueprint"] } once_cell = "1.14" tracing = "0.1.37" tracing-subscriber = "0.3" -adw = { version = "0.4", package = "libadwaita", features = ["v1_4"] } +adw = { version = "0.5", package = "libadwaita", features = ["v1_4"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +capnp = "0.17.2" +capnp-rpc = "0.17.0" +anyhow = "1.0.71" +chrono = "0.4.26" +rand = "0.8.5" +ureq = "2.7.1" +futures = "0.3.0" +ashpd = "0.6.0" diff --git a/README.md b/README.md index d26c289..bf004f1 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,8 @@ -# GTK + Libadwaita + Blueprint + Rust + Meson + Flatpak = <3 +# Notify -A boilerplate template to get started with GTK, Libadwaita, Blueprint, Rust, Meson, Flatpak made for GNOME. +https://ntfy.sh client application to receive everyday's notifications. -![Main window](data/resources/screenshots/screenshot1.png "Main window") +## Architecture -## What does it contains? +The code is split between the GUI and the underlying ntfy-daemon. -- A simple window with a headerbar -- Bunch of useful files that you SHOULD ship with your application on Linux: - - Metainfo: describe your application for the different application stores out there; - - Desktop: the application launcher; - - Icons: This repo contains three icons, a normal, a nightly & monochromatic icon (symbolic) per the GNOME HIG, exported using [App Icon Preview](https://flathub.org/apps/details/org.gnome.design.AppIconPreview). -- Flatpak Manifest for nightly builds -- Dual installation support -- Uses Meson for building the application -- Bundles the UI files & the CSS using gresources -- A pre-commit hook to run rustfmt on your code -- Tests to validate your Metainfo, Schemas & Desktop files -- Gsettings to store the window state, more settings could be added -- Gitlab CI to produce flatpak nightlies -- i18n support - -## How to init a project ? - -The template ships a simple python script to init a project easily. It asks you a few questions and replaces & renames all the necessary files. - -The script requires having `git` installed on your system. - -You can run it with, - -```shell -python3 create-project.py -``` - -```shell -➜ python3 create-project.py -Welcome to GTK Rust Template -Name: Contrast -Project Name: contrast -Application ID (e.g. org.domain.MyAwesomeApp, see: https://developer.gnome.org/ChooseApplicationID/): org.gnome.design.Contrast -Author: Bilal Elmoussaoui -Email: bil.elmoussaoui@gmail.com -``` - -A new directory named `contrast` containing the generated project - -## Building the project - -Make sure you have `flatpak` and `flatpak-builder` installed. Then run the commands below. Replace `` with the value you entered during project creation. Please note that these commands are just for demonstration purposes. Normally this would be handled by your IDE, such as GNOME Builder or VS Code with the Flatpak extension. - -``` -flatpak install --user org.gnome.Sdk//master org.freedesktop.Sdk.Extension.rust-stable org.gnome.Platform//master org.freedesktop.Sdk.Extension.llvm16 -flatpak-builder --user flatpak_app build-aux/.Devel.json -``` - -## Running the project - -Once the project is build, run the command below. Replace Replace `` and `` with the values you entered during project creation. Please note that these commands are just for demonstration purposes. Normally this would be handled by your IDE, such as GNOME Builder or VS Code with the Flatpak extension. - -``` -flatpak-builder --run flatpak_app build-aux/.Devel.json -``` - -## Community - -Join the GNOME and gtk-rs community! -- [Matrix chat](https://matrix.to/#/#rust:gnome.org): chat with other developers using gtk-rs -- [Discourse forum](https://discourse.gnome.org/tag/rust): topics tagged with `rust` on the GNOME forum. -- [GNOME circle](https://circle.gnome.org/): take inspiration from applications and libraries already extending the GNOME ecosystem. - -## Credits - -- [Podcasts](https://gitlab.gnome.org/World/podcasts) -- [Shortwave](https://gitlab.gnome.org/World/Shortwave) -- [gtk-rust-template without libadwaita and blueprint](https://gitlab.gnome.org/World/Rust/gtk-rust-template) diff --git a/build-aux/com.ranfdev.Notify.Devel.json b/build-aux/com.ranfdev.Notify.Devel.json index a28651f..1942e89 100644 --- a/build-aux/com.ranfdev.Notify.Devel.json +++ b/build-aux/com.ranfdev.Notify.Devel.json @@ -10,12 +10,15 @@ "command": "notify", "finish-args": [ "--share=ipc", + "--share=network", "--socket=fallback-x11", "--socket=wayland", + "--socket=session-bus", "--device=dri", - "--env=RUST_LOG=notify=debug", + "--env=RUST_LOG=notify=debug,ntfy_daemon=debug", "--env=G_MESSAGES_DEBUG=none", - "--env=RUST_BACKTRACE=1" + "--env=RUST_BACKTRACE=1", + "--talk-name=org.freedesktop.Notifications" ], "build-options": { "append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm16/bin", @@ -35,6 +38,17 @@ ] }, "modules": [ + { + "name": "capnp", + "buildsystem": "cmake", + "sources": [ + { + "type": "archive", + "url": "https://capnproto.org/capnproto-c++-0.10.4.tar.gz", + "sha256": "981e7ef6dbe3ac745907e55a78870fbb491c5d23abd4ebc04e20ec235af4458c" + } + ] + }, { "name": "notify", "buildsystem": "meson", diff --git a/data/icons/com.ranfdev.Notify-symbolic.svg b/data/icons/com.ranfdev.Notify-symbolic.svg index fc4d934..9184611 100644 --- a/data/icons/com.ranfdev.Notify-symbolic.svg +++ b/data/icons/com.ranfdev.Notify-symbolic.svg @@ -1,59 +1,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/data/icons/com.ranfdev.Notify.Devel.svg b/data/icons/com.ranfdev.Notify.Devel.svg index 92533ae..37219f1 100644 --- a/data/icons/com.ranfdev.Notify.Devel.svg +++ b/data/icons/com.ranfdev.Notify.Devel.svg @@ -1,147 +1,75 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/com.ranfdev.Notify.Source.svg b/data/icons/com.ranfdev.Notify.Source.svg new file mode 100644 index 0000000..241f17c --- /dev/null +++ b/data/icons/com.ranfdev.Notify.Source.svg @@ -0,0 +1,3172 @@ + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + GNOME Design Team + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hicolor + Symbolic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + battery is full and there is no a/c connected. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/com.ranfdev.Notify.svg b/data/icons/com.ranfdev.Notify.svg index c2bd5b1..cc4e8d5 100644 --- a/data/icons/com.ranfdev.Notify.svg +++ b/data/icons/com.ranfdev.Notify.svg @@ -1,60 +1,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/dice3-symbolic.svg b/data/icons/dice3-symbolic.svg new file mode 100644 index 0000000..22f474c --- /dev/null +++ b/data/icons/dice3-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/dice3-symbolic.svg b/data/resources/dice3-symbolic.svg new file mode 100644 index 0000000..22f474c --- /dev/null +++ b/data/resources/dice3-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/meson.build b/data/resources/meson.build index e55b4ae..f56f78c 100644 --- a/data/resources/meson.build +++ b/data/resources/meson.build @@ -4,6 +4,7 @@ blueprints = custom_target('blueprints', input: files( 'ui/window.blp', 'ui/shortcuts.blp', + 'ui/subscription_info_dialog.blp', ), output: '.', command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index d62cd5d..651be08 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -4,6 +4,11 @@ ui/shortcuts.ui ui/window.ui + ui/subscription_info_dialog.ui style.css + + + + ../icons/dice3-symbolic.svg diff --git a/data/resources/style.css b/data/resources/style.css index 3c4bd47..0f82a8c 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -2,3 +2,30 @@ font-size: 36px; font-weight: bold; } + +.chip { + min-height: 16px; + min-width: 16px; + padding: 3px 5px; + color: @theme_fg_color; + border-radius: 8px; +} +.chip--warning { + background: alpha(@yellow_2, 0.2); +} +.chip--info { + color: @blue_5; + background: alpha(@blue_2, 0.2); +} +.chip--degraded { + color: @orange_5; + background: alpha(@orange_2, 0.2); +} + +.chip--danger { + background: alpha(@red_2, 0.2); +} + +.chip--small { + font-size: 0.8rem; +} diff --git a/data/resources/ui/subscription_info_dialog.blp b/data/resources/ui/subscription_info_dialog.blp new file mode 100644 index 0000000..823574a --- /dev/null +++ b/data/resources/ui/subscription_info_dialog.blp @@ -0,0 +1,50 @@ +using Gtk 4.0; +using Adw 1; + +template $SubscriptionInfoDialog : Adw.Window { + modal: true; + title: "Subscription Info"; + width-request: 240; + Adw.ToolbarView { + [top] + Adw.HeaderBar {} + Adw.Clamp { + Gtk.Box { + orientation: vertical; + spacing: 8; + margin-top: 8; + margin-bottom: 8; + margin-start: 8; + margin-end: 8; + Gtk.ListBox { + Adw.EntryRow display_name_entry { + title: "Display Name"; + } + Adw.ActionRow { + title: "Topic"; + subtitle-selectable: true; + subtitle: bind (template.subscription as <$TopicSubscription>).topic as ; + styles [ + "property" + ] + } + Adw.ActionRow { + title: "Server"; + subtitle: bind (template.subscription as <$TopicSubscription>).server as ; + subtitle-selectable: true; + styles [ + "property" + ] + } + Adw.SwitchRow muted_switch_row { + title: "Muted"; + } + + styles [ + "boxed-list" + ] + } + } + } + } +} diff --git a/data/resources/ui/window.blp b/data/resources/ui/window.blp index 6360934..2264e09 100644 --- a/data/resources/ui/window.blp +++ b/data/resources/ui/window.blp @@ -20,60 +20,126 @@ menu primary_menu { } } -template $ExampleApplicationWindow : Adw.ApplicationWindow { - width-request: 240; - height-request: 480; - Adw.Breakpoint { - condition ("max-width: 400sp") - setters { - split_view.collapsed: true; - show_hello_btn.visible: true; +menu subscription_menu { + section { + item { + label: _("_Subscription Info"); + action: "win.show-subscription-info"; } } - Adw.NavigationSplitView split_view { - sidebar: Adw.NavigationPage { - title: "Sidebar"; - child: Adw.ToolbarView { - [top] - Adw.HeaderBar { - [end] - MenuButton appmenu_button { - icon-name: "open-menu-symbolic"; - menu-model: primary_menu; - primary: true; - tooltip-text: _("Main Menu"); - } - } + section { + item { + label: _("_Clear all notifications"); + action: "win.clear-notifications"; + } - content: Adw.StatusPage { - title: "Sidebar"; - child: Gtk.Button show_hello_btn { - label: "Show Hello Page"; - visible: false; - styles [ - "pill" - ] - action-name: "navigation.push"; - action-target: "'hello'"; - }; - }; - }; - }; - content: Adw.NavigationPage { - title: "Hello Page"; - tag: "hello"; - child: Adw.ToolbarView { - [top] - Adw.HeaderBar headerbar { - // this will have a title label inherited from the Adw.NavigationPage - } - - content: Adw.StatusPage { - title: "Hello World!"; - description: "Hello to everyone!"; - }; - }; - }; + item { + label: _("_Unsubscribe"); + action: "win.unsubscribe"; + } + } +} + +template $NotifyWindow : Adw.ApplicationWindow { + width-request: 240; + height-request: 360; + Adw.Breakpoint { + condition ("max-width: 640sp") + setters { + navigation_split_view.collapsed: true; + } + } + Adw.ToastOverlay toast_overlay { + Adw.NavigationSplitView navigation_split_view { + sidebar: Adw.NavigationPage { + title: "Topics"; + child: Adw.ToolbarView { + [top] + Adw.HeaderBar { + [start] + Button { + icon-name: "list-add-symbolic"; + clicked => $show_add_topic() swapped; + } + [end] + MenuButton appmenu_button { + icon-name: "open-menu-symbolic"; + menu-model: primary_menu; + primary: true; + tooltip-text: _("Main Menu"); + } + } + + Gtk.Stack stack { + Adw.StatusPage welcome_view { + title: "Notify"; + description: "Subscribe to one topic and start listening for notifications"; + child: Gtk.Button { + label: "Subscribe To Topic"; + clicked => $show_add_topic() swapped; + halign: center; + styles [ + "suggested-action", + "pill" + ] + }; + } + ScrolledWindow list_view { + propagate-natural-height: true; + ListBox subscription_list { + styles [ + "navigation-sidebar" + ] + } + } + } + }; + }; + content: Adw.NavigationPage { + title: "Notifications"; + Adw.ToolbarView subscription_view { + [top] + Adw.HeaderBar headerbar { + [end] + MenuButton subscription_menu_btn { + icon-name: "view-more-symbolic"; + menu-model: subscription_menu; + tooltip-text: _("Subscription Menu"); + } + } + [top] + Adw.Banner banner { + title: "Reconnecting..."; + } + + content: ScrolledWindow message_scroll { + propagate-natural-height: true; + vexpand: true; + Adw.Clamp { + ListBox message_list { + selection-mode: none; + show-separators: true; + styles [ + "background" + ] + } + } + }; + [bottom] + Adw.Bin { + margin-top: 8; + margin-bottom: 8; + margin-start: 8; + margin-end: 8; + Adw.Clamp { + Entry entry { + placeholder-text: "Message..."; + } + } + } + } + }; + } } } diff --git a/ntfy-daemon/.gitignore b/ntfy-daemon/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/ntfy-daemon/.gitignore @@ -0,0 +1 @@ +/target diff --git a/ntfy-daemon/Cargo.lock b/ntfy-daemon/Cargo.lock new file mode 100644 index 0000000..397bcfa --- /dev/null +++ b/ntfy-daemon/Cargo.lock @@ -0,0 +1,2312 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.23", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.37.23", + "signal-hook", + "windows-sys", +] + +[[package]] +name = "async-recursion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[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.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "log", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "capnp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e65021d89250bbfe7c2791789ced2c4bdc21b0e8bb59c64f3fd6145a5fd678" + +[[package]] +name = "capnp-futures" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d520e0af228b92de357f230f4987ee4f9786f2b8aa24b9cfe53f5b11c17198" +dependencies = [ + "capnp", + "futures", +] + +[[package]] +name = "capnp-rpc" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab8e869783e491cbcc350427a5e775aa4d8a1deaa5198d74332957cfa430779" +dependencies = [ + "capnp", + "capnp-futures", + "futures", +] + +[[package]] +name = "capnpc" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbc3763fb3e6635188e9cc51ee11a26f8777c553ca377430818dbebaaf6042b" +dependencies = [ + "capnp", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.3", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "mac-notification-sys" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e72d50edb17756489e79d52eb146927bec8eba9dd48faadf9ef08bca3791ad5" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "static_assertions", +] + +[[package]] +name = "notify-rust" +version = "4.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfa211d18e360f08e36c364308f394b5eb23a6629150690e109a916dc6f610e" +dependencies = [ + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "ntfy-daemon" +version = "0.1.0" +dependencies = [ + "anyhow", + "capnp", + "capnp-rpc", + "capnpc", + "clap", + "futures", + "notify-rust", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "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 2.0.23", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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 = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.3.3", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daf513456463b42aa1d94cff7e0c24d682b429f020b9afa4f5ba5c40a22b237" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69b106b68bc8054f0e974e70d19984040f8a5cf9215ca82626ea4853f82c4b9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "serde_json" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5bff1d532fead7c43324a0fa33643b8621a47ce2944a633be4cb6c0240898f" +dependencies = [ + "quick-xml", + "windows", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix 0.37.23", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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 = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[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.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.23", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "zbus" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zvariant" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/ntfy-daemon/Cargo.toml b/ntfy-daemon/Cargo.toml new file mode 100644 index 0000000..cc0f459 --- /dev/null +++ b/ntfy-daemon/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "ntfy-daemon" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[build-dependencies] +capnpc = "0.17.2" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +capnp = "0.17.2" +capnp-rpc = "0.17.0" +futures = "0.3.0" +tokio = { version = "1.0.0", features = ["net", "rt", "macros", "parking_lot"]} +tokio-util = { version = "0.7.4", features = ["compat", "io"] } +clap = { version = "4.3.11", features = ["derive"] } +anyhow = "1.0.71" +tokio-stream = { version = "0.1.14", features = ["io-util", "time"] } +rusqlite = "0.29.0" +rand = "0.8.5" +reqwest = { version = "0.11.18", features = ["stream", "rustls-tls"]} +url = "2.4.0" +ashpd = "0.6.0" +generational-arena = "0.2.9" +tracing = "0.1.37" +thiserror = "1.0.49" +regex = "1.9.6" diff --git a/ntfy-daemon/README.md b/ntfy-daemon/README.md new file mode 100644 index 0000000..7161820 --- /dev/null +++ b/ntfy-daemon/README.md @@ -0,0 +1,5 @@ +# ntfy-daemon + +Rust crate providing a capnp-rpc interface to multiple ntfy servers. +Connections to the same server are multiplexed over http2. +Messages are received and stored in a sqlite database for persistance. diff --git a/ntfy-daemon/build.rs b/ntfy-daemon/build.rs new file mode 100644 index 0000000..cd7eb77 --- /dev/null +++ b/ntfy-daemon/build.rs @@ -0,0 +1,6 @@ +fn main() { + capnpc::CompilerCommand::new() + .file("src/ntfy.capnp") + .run() + .unwrap(); +} diff --git a/ntfy-daemon/data/mailer_emoji_map.json b/ntfy-daemon/data/mailer_emoji_map.json new file mode 100644 index 0000000..8520c24 --- /dev/null +++ b/ntfy-daemon/data/mailer_emoji_map.json @@ -0,0 +1,1857 @@ +{ + "+1": "👍", + "-1": "👎", + "100": "💯", + "1234": "🔢", + "1st_place_medal": "🥇", + "2nd_place_medal": "🥈", + "3rd_place_medal": "🥉", + "8ball": "🎱", + "a": "🅰️", + "ab": "🆎", + "abacus": "🧮", + "abc": "🔤", + "abcd": "🔡", + "accept": "🉑", + "accordion": "🪗", + "adhesive_bandage": "🩹", + "adult": "🧑", + "aerial_tramway": "🚡", + "afghanistan": "🇦🇫", + "airplane": "✈️", + "aland_islands": "🇦🇽", + "alarm_clock": "⏰", + "albania": "🇦🇱", + "alembic": "⚗️", + "algeria": "🇩🇿", + "alien": "👽", + "ambulance": "🚑", + "american_samoa": "🇦🇸", + "amphora": "🏺", + "anatomical_heart": "🫀", + "anchor": "⚓", + "andorra": "🇦🇩", + "angel": "👼", + "anger": "💢", + "angola": "🇦🇴", + "angry": "😠", + "anguilla": "🇦🇮", + "anguished": "😧", + "ant": "🐜", + "antarctica": "🇦🇶", + "antigua_barbuda": "🇦🇬", + "apple": "🍎", + "aquarius": "♒", + "argentina": "🇦🇷", + "aries": "♈", + "armenia": "🇦🇲", + "arrow_backward": "◀️", + "arrow_double_down": "⏬", + "arrow_double_up": "⏫", + "arrow_down": "⬇️", + "arrow_down_small": "🔽", + "arrow_forward": "▶️", + "arrow_heading_down": "⤵️", + "arrow_heading_up": "⤴️", + "arrow_left": "⬅️", + "arrow_lower_left": "↙️", + "arrow_lower_right": "↘️", + "arrow_right": "➡️", + "arrow_right_hook": "↪️", + "arrow_up": "⬆️", + "arrow_up_down": "↕️", + "arrow_up_small": "🔼", + "arrow_upper_left": "↖️", + "arrow_upper_right": "↗️", + "arrows_clockwise": "🔃", + "arrows_counterclockwise": "🔄", + "art": "🎨", + "articulated_lorry": "🚛", + "artificial_satellite": "🛰️", + "artist": "🧑‍🎨", + "aruba": "🇦🇼", + "ascension_island": "🇦🇨", + "asterisk": "*️⃣", + "astonished": "😲", + "astronaut": "🧑‍🚀", + "athletic_shoe": "👟", + "atm": "🏧", + "atom_symbol": "⚛️", + "australia": "🇦🇺", + "austria": "🇦🇹", + "auto_rickshaw": "🛺", + "avocado": "🥑", + "axe": "🪓", + "azerbaijan": "🇦🇿", + "b": "🅱️", + "baby": "👶", + "baby_bottle": "🍼", + "baby_chick": "🐤", + "baby_symbol": "🚼", + "back": "🔙", + "bacon": "🥓", + "badger": "🦡", + "badminton": "🏸", + "bagel": "🥯", + "baggage_claim": "🛄", + "baguette_bread": "🥖", + "bahamas": "🇧🇸", + "bahrain": "🇧🇭", + "balance_scale": "⚖️", + "bald_man": "👨‍🦲", + "bald_woman": "👩‍🦲", + "ballet_shoes": "🩰", + "balloon": "🎈", + "ballot_box": "🗳️", + "ballot_box_with_check": "☑️", + "bamboo": "🎍", + "banana": "🍌", + "bangbang": "‼️", + "bangladesh": "🇧🇩", + "banjo": "🪕", + "bank": "🏦", + "bar_chart": "📊", + "barbados": "🇧🇧", + "barber": "💈", + "baseball": "⚾", + "basket": "🧺", + "basketball": "🏀", + "basketball_man": "⛹️‍♂️", + "basketball_woman": "⛹️‍♀️", + "bat": "🦇", + "bath": "🛀", + "bathtub": "🛁", + "battery": "🔋", + "beach_umbrella": "🏖️", + "bear": "🐻", + "bearded_person": "🧔", + "beaver": "🦫", + "bed": "🛏️", + "bee": "🐝", + "beer": "🍺", + "beers": "🍻", + "beetle": "🪲", + "beginner": "🔰", + "belarus": "🇧🇾", + "belgium": "🇧🇪", + "belize": "🇧🇿", + "bell": "🔔", + "bell_pepper": "🫑", + "bellhop_bell": "🛎️", + "benin": "🇧🇯", + "bento": "🍱", + "bermuda": "🇧🇲", + "beverage_box": "🧃", + "bhutan": "🇧🇹", + "bicyclist": "🚴", + "bike": "🚲", + "biking_man": "🚴‍♂️", + "biking_woman": "🚴‍♀️", + "bikini": "👙", + "billed_cap": "🧢", + "biohazard": "☣️", + "bird": "🐦", + "birthday": "🎂", + "bison": "🦬", + "black_cat": "🐈‍⬛", + "black_circle": "⚫", + "black_flag": "🏴", + "black_heart": "🖤", + "black_joker": "🃏", + "black_large_square": "⬛", + "black_medium_small_square": "◾", + "black_medium_square": "◼️", + "black_nib": "✒️", + "black_small_square": "▪️", + "black_square_button": "🔲", + "blond_haired_man": "👱‍♂️", + "blond_haired_person": "👱", + "blond_haired_woman": "👱‍♀️", + "blonde_woman": "👱‍♀️", + "blossom": "🌼", + "blowfish": "🐡", + "blue_book": "📘", + "blue_car": "🚙", + "blue_heart": "💙", + "blue_square": "🟦", + "blueberries": "🫐", + "blush": "😊", + "boar": "🐗", + "boat": "⛵", + "bolivia": "🇧🇴", + "bomb": "💣", + "bone": "🦴", + "book": "📖", + "bookmark": "🔖", + "bookmark_tabs": "📑", + "books": "📚", + "boom": "💥", + "boomerang": "🪃", + "boot": "👢", + "bosnia_herzegovina": "🇧🇦", + "botswana": "🇧🇼", + "bouncing_ball_man": "⛹️‍♂️", + "bouncing_ball_person": "⛹️", + "bouncing_ball_woman": "⛹️‍♀️", + "bouquet": "💐", + "bouvet_island": "🇧🇻", + "bow": "🙇", + "bow_and_arrow": "🏹", + "bowing_man": "🙇‍♂️", + "bowing_woman": "🙇‍♀️", + "bowl_with_spoon": "🥣", + "bowling": "🎳", + "boxing_glove": "🥊", + "boy": "👦", + "brain": "🧠", + "brazil": "🇧🇷", + "bread": "🍞", + "breast_feeding": "🤱", + "bricks": "🧱", + "bride_with_veil": "👰‍♀️", + "bridge_at_night": "🌉", + "briefcase": "💼", + "british_indian_ocean_territory": "🇮🇴", + "british_virgin_islands": "🇻🇬", + "broccoli": "🥦", + "broken_heart": "💔", + "broom": "🧹", + "brown_circle": "🟤", + "brown_heart": "🤎", + "brown_square": "🟫", + "brunei": "🇧🇳", + "bubble_tea": "🧋", + "bucket": "🪣", + "bug": "🐛", + "building_construction": "🏗️", + "bulb": "💡", + "bulgaria": "🇧🇬", + "bullettrain_front": "🚅", + "bullettrain_side": "🚄", + "burkina_faso": "🇧🇫", + "burrito": "🌯", + "burundi": "🇧🇮", + "bus": "🚌", + "business_suit_levitating": "🕴️", + "busstop": "🚏", + "bust_in_silhouette": "👤", + "busts_in_silhouette": "👥", + "butter": "🧈", + "butterfly": "🦋", + "cactus": "🌵", + "cake": "🍰", + "calendar": "📆", + "call_me_hand": "🤙", + "calling": "📲", + "cambodia": "🇰🇭", + "camel": "🐫", + "camera": "📷", + "camera_flash": "📸", + "cameroon": "🇨🇲", + "camping": "🏕️", + "canada": "🇨🇦", + "canary_islands": "🇮🇨", + "cancer": "♋", + "candle": "🕯️", + "candy": "🍬", + "canned_food": "🥫", + "canoe": "🛶", + "cape_verde": "🇨🇻", + "capital_abcd": "🔠", + "capricorn": "♑", + "car": "🚗", + "card_file_box": "🗃️", + "card_index": "📇", + "card_index_dividers": "🗂️", + "caribbean_netherlands": "🇧🇶", + "carousel_horse": "🎠", + "carpentry_saw": "🪚", + "carrot": "🥕", + "cartwheeling": "🤸", + "cat": "🐱", + "cat2": "🐈", + "cayman_islands": "🇰🇾", + "cd": "💿", + "central_african_republic": "🇨🇫", + "ceuta_melilla": "🇪🇦", + "chad": "🇹🇩", + "chains": "⛓️", + "chair": "🪑", + "champagne": "🍾", + "chart": "💹", + "chart_with_downwards_trend": "📉", + "chart_with_upwards_trend": "📈", + "checkered_flag": "🏁", + "cheese": "🧀", + "cherries": "🍒", + "cherry_blossom": "🌸", + "chess_pawn": "♟️", + "chestnut": "🌰", + "chicken": "🐔", + "child": "🧒", + "children_crossing": "🚸", + "chile": "🇨🇱", + "chipmunk": "🐿️", + "chocolate_bar": "🍫", + "chopsticks": "🥢", + "christmas_island": "🇨🇽", + "christmas_tree": "🎄", + "church": "⛪", + "cinema": "🎦", + "circus_tent": "🎪", + "city_sunrise": "🌇", + "city_sunset": "🌆", + "cityscape": "🏙️", + "cl": "🆑", + "clamp": "🗜️", + "clap": "👏", + "clapper": "🎬", + "classical_building": "🏛️", + "climbing": "🧗", + "climbing_man": "🧗‍♂️", + "climbing_woman": "🧗‍♀️", + "clinking_glasses": "🥂", + "clipboard": "📋", + "clipperton_island": "🇨🇵", + "clock1": "🕐", + "clock10": "🕙", + "clock1030": "🕥", + "clock11": "🕚", + "clock1130": "🕦", + "clock12": "🕛", + "clock1230": "🕧", + "clock130": "🕜", + "clock2": "🕑", + "clock230": "🕝", + "clock3": "🕒", + "clock330": "🕞", + "clock4": "🕓", + "clock430": "🕟", + "clock5": "🕔", + "clock530": "🕠", + "clock6": "🕕", + "clock630": "🕡", + "clock7": "🕖", + "clock730": "🕢", + "clock8": "🕗", + "clock830": "🕣", + "clock9": "🕘", + "clock930": "🕤", + "closed_book": "📕", + "closed_lock_with_key": "🔐", + "closed_umbrella": "🌂", + "cloud": "☁️", + "cloud_with_lightning": "🌩️", + "cloud_with_lightning_and_rain": "⛈️", + "cloud_with_rain": "🌧️", + "cloud_with_snow": "🌨️", + "clown_face": "🤡", + "clubs": "♣️", + "cn": "🇨🇳", + "coat": "🧥", + "cockroach": "🪳", + "cocktail": "🍸", + "coconut": "🥥", + "cocos_islands": "🇨🇨", + "coffee": "☕", + "coffin": "⚰️", + "coin": "🪙", + "cold_face": "🥶", + "cold_sweat": "😰", + "collision": "💥", + "colombia": "🇨🇴", + "comet": "☄️", + "comoros": "🇰🇲", + "compass": "🧭", + "computer": "💻", + "computer_mouse": "🖱️", + "confetti_ball": "🎊", + "confounded": "😖", + "confused": "😕", + "congo_brazzaville": "🇨🇬", + "congo_kinshasa": "🇨🇩", + "congratulations": "㊗️", + "construction": "🚧", + "construction_worker": "👷", + "construction_worker_man": "👷‍♂️", + "construction_worker_woman": "👷‍♀️", + "control_knobs": "🎛️", + "convenience_store": "🏪", + "cook": "🧑‍🍳", + "cook_islands": "🇨🇰", + "cookie": "🍪", + "cool": "🆒", + "cop": "👮", + "copyright": "©️", + "corn": "🌽", + "costa_rica": "🇨🇷", + "cote_divoire": "🇨🇮", + "couch_and_lamp": "🛋️", + "couple": "👫", + "couple_with_heart": "💑", + "couple_with_heart_man_man": "👨‍❤️‍👨", + "couple_with_heart_woman_man": "👩‍❤️‍👨", + "couple_with_heart_woman_woman": "👩‍❤️‍👩", + "couplekiss": "💏", + "couplekiss_man_man": "👨‍❤️‍💋‍👨", + "couplekiss_man_woman": "👩‍❤️‍💋‍👨", + "couplekiss_woman_woman": "👩‍❤️‍💋‍👩", + "cow": "🐮", + "cow2": "🐄", + "cowboy_hat_face": "🤠", + "crab": "🦀", + "crayon": "🖍️", + "credit_card": "💳", + "crescent_moon": "🌙", + "cricket": "🦗", + "cricket_game": "🏏", + "croatia": "🇭🇷", + "crocodile": "🐊", + "croissant": "🥐", + "crossed_fingers": "🤞", + "crossed_flags": "🎌", + "crossed_swords": "⚔️", + "crown": "👑", + "cry": "😢", + "crying_cat_face": "😿", + "crystal_ball": "🔮", + "cuba": "🇨🇺", + "cucumber": "🥒", + "cup_with_straw": "🥤", + "cupcake": "🧁", + "cupid": "💘", + "curacao": "🇨🇼", + "curling_stone": "🥌", + "curly_haired_man": "👨‍🦱", + "curly_haired_woman": "👩‍🦱", + "curly_loop": "➰", + "currency_exchange": "💱", + "curry": "🍛", + "cursing_face": "🤬", + "custard": "🍮", + "customs": "🛃", + "cut_of_meat": "🥩", + "cyclone": "🌀", + "cyprus": "🇨🇾", + "czech_republic": "🇨🇿", + "dagger": "🗡️", + "dancer": "💃", + "dancers": "👯", + "dancing_men": "👯‍♂️", + "dancing_women": "👯‍♀️", + "dango": "🍡", + "dark_sunglasses": "🕶️", + "dart": "🎯", + "dash": "💨", + "date": "📅", + "de": "🇩🇪", + "deaf_man": "🧏‍♂️", + "deaf_person": "🧏", + "deaf_woman": "🧏‍♀️", + "deciduous_tree": "🌳", + "deer": "🦌", + "denmark": "🇩🇰", + "department_store": "🏬", + "derelict_house": "🏚️", + "desert": "🏜️", + "desert_island": "🏝️", + "desktop_computer": "🖥️", + "detective": "🕵️", + "diamond_shape_with_a_dot_inside": "💠", + "diamonds": "♦️", + "diego_garcia": "🇩🇬", + "disappointed": "😞", + "disappointed_relieved": "😥", + "disguised_face": "🥸", + "diving_mask": "🤿", + "diya_lamp": "🪔", + "dizzy": "💫", + "dizzy_face": "😵", + "djibouti": "🇩🇯", + "dna": "🧬", + "do_not_litter": "🚯", + "dodo": "🦤", + "dog": "🐶", + "dog2": "🐕", + "dollar": "💵", + "dolls": "🎎", + "dolphin": "🐬", + "dominica": "🇩🇲", + "dominican_republic": "🇩🇴", + "door": "🚪", + "doughnut": "🍩", + "dove": "🕊️", + "dragon": "🐉", + "dragon_face": "🐲", + "dress": "👗", + "dromedary_camel": "🐪", + "drooling_face": "🤤", + "drop_of_blood": "🩸", + "droplet": "💧", + "drum": "🥁", + "duck": "🦆", + "dumpling": "🥟", + "dvd": "📀", + "e-mail": "📧", + "eagle": "🦅", + "ear": "👂", + "ear_of_rice": "🌾", + "ear_with_hearing_aid": "🦻", + "earth_africa": "🌍", + "earth_americas": "🌎", + "earth_asia": "🌏", + "ecuador": "🇪🇨", + "egg": "🥚", + "eggplant": "🍆", + "egypt": "🇪🇬", + "eight": "8️⃣", + "eight_pointed_black_star": "✴️", + "eight_spoked_asterisk": "✳️", + "eject_button": "⏏️", + "el_salvador": "🇸🇻", + "electric_plug": "🔌", + "elephant": "🐘", + "elevator": "🛗", + "elf": "🧝", + "elf_man": "🧝‍♂️", + "elf_woman": "🧝‍♀️", + "email": "📧", + "end": "🔚", + "england": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "envelope": "✉️", + "envelope_with_arrow": "📩", + "equatorial_guinea": "🇬🇶", + "eritrea": "🇪🇷", + "es": "🇪🇸", + "estonia": "🇪🇪", + "ethiopia": "🇪🇹", + "eu": "🇪🇺", + "euro": "💶", + "european_castle": "🏰", + "european_post_office": "🏤", + "european_union": "🇪🇺", + "evergreen_tree": "🌲", + "exclamation": "❗", + "exploding_head": "🤯", + "expressionless": "😑", + "eye": "👁️", + "eye_speech_bubble": "👁️‍🗨️", + "eyeglasses": "👓", + "eyes": "👀", + "face_exhaling": "😮‍💨", + "face_in_clouds": "😶‍🌫️", + "face_with_head_bandage": "🤕", + "face_with_spiral_eyes": "😵‍💫", + "face_with_thermometer": "🤒", + "facepalm": "🤦", + "facepunch": "👊", + "factory": "🏭", + "factory_worker": "🧑‍🏭", + "fairy": "🧚", + "fairy_man": "🧚‍♂️", + "fairy_woman": "🧚‍♀️", + "falafel": "🧆", + "falkland_islands": "🇫🇰", + "fallen_leaf": "🍂", + "family": "👪", + "family_man_boy": "👨‍👦", + "family_man_boy_boy": "👨‍👦‍👦", + "family_man_girl": "👨‍👧", + "family_man_girl_boy": "👨‍👧‍👦", + "family_man_girl_girl": "👨‍👧‍👧", + "family_man_man_boy": "👨‍👨‍👦", + "family_man_man_boy_boy": "👨‍👨‍👦‍👦", + "family_man_man_girl": "👨‍👨‍👧", + "family_man_man_girl_boy": "👨‍👨‍👧‍👦", + "family_man_man_girl_girl": "👨‍👨‍👧‍👧", + "family_man_woman_boy": "👨‍👩‍👦", + "family_man_woman_boy_boy": "👨‍👩‍👦‍👦", + "family_man_woman_girl": "👨‍👩‍👧", + "family_man_woman_girl_boy": "👨‍👩‍👧‍👦", + "family_man_woman_girl_girl": "👨‍👩‍👧‍👧", + "family_woman_boy": "👩‍👦", + "family_woman_boy_boy": "👩‍👦‍👦", + "family_woman_girl": "👩‍👧", + "family_woman_girl_boy": "👩‍👧‍👦", + "family_woman_girl_girl": "👩‍👧‍👧", + "family_woman_woman_boy": "👩‍👩‍👦", + "family_woman_woman_boy_boy": "👩‍👩‍👦‍👦", + "family_woman_woman_girl": "👩‍👩‍👧", + "family_woman_woman_girl_boy": "👩‍👩‍👧‍👦", + "family_woman_woman_girl_girl": "👩‍👩‍👧‍👧", + "farmer": "🧑‍🌾", + "faroe_islands": "🇫🇴", + "fast_forward": "⏩", + "fax": "📠", + "fearful": "😨", + "feather": "🪶", + "feet": "🐾", + "female_detective": "🕵️‍♀️", + "female_sign": "♀️", + "ferris_wheel": "🎡", + "ferry": "⛴️", + "field_hockey": "🏑", + "fiji": "🇫🇯", + "file_cabinet": "🗄️", + "file_folder": "📁", + "film_projector": "📽️", + "film_strip": "🎞️", + "finland": "🇫🇮", + "fire": "🔥", + "fire_engine": "🚒", + "fire_extinguisher": "🧯", + "firecracker": "🧨", + "firefighter": "🧑‍🚒", + "fireworks": "🎆", + "first_quarter_moon": "🌓", + "first_quarter_moon_with_face": "🌛", + "fish": "🐟", + "fish_cake": "🍥", + "fishing_pole_and_fish": "🎣", + "fist": "✊", + "fist_left": "🤛", + "fist_oncoming": "👊", + "fist_raised": "✊", + "fist_right": "🤜", + "five": "5️⃣", + "flags": "🎏", + "flamingo": "🦩", + "flashlight": "🔦", + "flat_shoe": "🥿", + "flatbread": "🫓", + "fleur_de_lis": "⚜️", + "flight_arrival": "🛬", + "flight_departure": "🛫", + "flipper": "🐬", + "floppy_disk": "💾", + "flower_playing_cards": "🎴", + "flushed": "😳", + "fly": "🪰", + "flying_disc": "🥏", + "flying_saucer": "🛸", + "fog": "🌫️", + "foggy": "🌁", + "fondue": "🫕", + "foot": "🦶", + "football": "🏈", + "footprints": "👣", + "fork_and_knife": "🍴", + "fortune_cookie": "🥠", + "fountain": "⛲", + "fountain_pen": "🖋️", + "four": "4️⃣", + "four_leaf_clover": "🍀", + "fox_face": "🦊", + "fr": "🇫🇷", + "framed_picture": "🖼️", + "free": "🆓", + "french_guiana": "🇬🇫", + "french_polynesia": "🇵🇫", + "french_southern_territories": "🇹🇫", + "fried_egg": "🍳", + "fried_shrimp": "🍤", + "fries": "🍟", + "frog": "🐸", + "frowning": "😦", + "frowning_face": "☹️", + "frowning_man": "🙍‍♂️", + "frowning_person": "🙍", + "frowning_woman": "🙍‍♀️", + "fu": "🖕", + "fuelpump": "⛽", + "full_moon": "🌕", + "full_moon_with_face": "🌝", + "funeral_urn": "⚱️", + "gabon": "🇬🇦", + "gambia": "🇬🇲", + "game_die": "🎲", + "garlic": "🧄", + "gb": "🇬🇧", + "gear": "⚙️", + "gem": "💎", + "gemini": "♊", + "genie": "🧞", + "genie_man": "🧞‍♂️", + "genie_woman": "🧞‍♀️", + "georgia": "🇬🇪", + "ghana": "🇬🇭", + "ghost": "👻", + "gibraltar": "🇬🇮", + "gift": "🎁", + "gift_heart": "💝", + "giraffe": "🦒", + "girl": "👧", + "globe_with_meridians": "🌐", + "gloves": "🧤", + "goal_net": "🥅", + "goat": "🐐", + "goggles": "🥽", + "golf": "⛳", + "golfing": "🏌️", + "golfing_man": "🏌️‍♂️", + "golfing_woman": "🏌️‍♀️", + "gorilla": "🦍", + "grapes": "🍇", + "greece": "🇬🇷", + "green_apple": "🍏", + "green_book": "📗", + "green_circle": "🟢", + "green_heart": "💚", + "green_salad": "🥗", + "green_square": "🟩", + "greenland": "🇬🇱", + "grenada": "🇬🇩", + "grey_exclamation": "❕", + "grey_question": "❔", + "grimacing": "😬", + "grin": "😁", + "grinning": "😀", + "guadeloupe": "🇬🇵", + "guam": "🇬🇺", + "guard": "💂", + "guardsman": "💂‍♂️", + "guardswoman": "💂‍♀️", + "guatemala": "🇬🇹", + "guernsey": "🇬🇬", + "guide_dog": "🦮", + "guinea": "🇬🇳", + "guinea_bissau": "🇬🇼", + "guitar": "🎸", + "gun": "🔫", + "guyana": "🇬🇾", + "haircut": "💇", + "haircut_man": "💇‍♂️", + "haircut_woman": "💇‍♀️", + "haiti": "🇭🇹", + "hamburger": "🍔", + "hammer": "🔨", + "hammer_and_pick": "⚒️", + "hammer_and_wrench": "🛠️", + "hamster": "🐹", + "hand": "✋", + "hand_over_mouth": "🤭", + "handbag": "👜", + "handball_person": "🤾", + "handshake": "🤝", + "hankey": "💩", + "hash": "#️⃣", + "hatched_chick": "🐥", + "hatching_chick": "🐣", + "headphones": "🎧", + "headstone": "🪦", + "health_worker": "🧑‍⚕️", + "hear_no_evil": "🙉", + "heard_mcdonald_islands": "🇭🇲", + "heart": "❤️", + "heart_decoration": "💟", + "heart_eyes": "😍", + "heart_eyes_cat": "😻", + "heart_on_fire": "❤️‍🔥", + "heartbeat": "💓", + "heartpulse": "💗", + "hearts": "♥️", + "heavy_check_mark": "✔️", + "heavy_division_sign": "➗", + "heavy_dollar_sign": "💲", + "heavy_exclamation_mark": "❗", + "heavy_heart_exclamation": "❣️", + "heavy_minus_sign": "➖", + "heavy_multiplication_x": "✖️", + "heavy_plus_sign": "➕", + "hedgehog": "🦔", + "helicopter": "🚁", + "herb": "🌿", + "hibiscus": "🌺", + "high_brightness": "🔆", + "high_heel": "👠", + "hiking_boot": "🥾", + "hindu_temple": "🛕", + "hippopotamus": "🦛", + "hocho": "🔪", + "hole": "🕳️", + "honduras": "🇭🇳", + "honey_pot": "🍯", + "honeybee": "🐝", + "hong_kong": "🇭🇰", + "hook": "🪝", + "horse": "🐴", + "horse_racing": "🏇", + "hospital": "🏥", + "hot_face": "🥵", + "hot_pepper": "🌶️", + "hotdog": "🌭", + "hotel": "🏨", + "hotsprings": "♨️", + "hourglass": "⌛", + "hourglass_flowing_sand": "⏳", + "house": "🏠", + "house_with_garden": "🏡", + "houses": "🏘️", + "hugs": "🤗", + "hungary": "🇭🇺", + "hushed": "😯", + "hut": "🛖", + "ice_cream": "🍨", + "ice_cube": "🧊", + "ice_hockey": "🏒", + "ice_skate": "⛸️", + "icecream": "🍦", + "iceland": "🇮🇸", + "id": "🆔", + "ideograph_advantage": "🉐", + "imp": "👿", + "inbox_tray": "📥", + "incoming_envelope": "📨", + "india": "🇮🇳", + "indonesia": "🇮🇩", + "infinity": "♾️", + "information_desk_person": "💁", + "information_source": "ℹ️", + "innocent": "😇", + "interrobang": "⁉️", + "iphone": "📱", + "iran": "🇮🇷", + "iraq": "🇮🇶", + "ireland": "🇮🇪", + "isle_of_man": "🇮🇲", + "israel": "🇮🇱", + "it": "🇮🇹", + "izakaya_lantern": "🏮", + "jack_o_lantern": "🎃", + "jamaica": "🇯🇲", + "japan": "🗾", + "japanese_castle": "🏯", + "japanese_goblin": "👺", + "japanese_ogre": "👹", + "jeans": "👖", + "jersey": "🇯🇪", + "jigsaw": "🧩", + "jordan": "🇯🇴", + "joy": "😂", + "joy_cat": "😹", + "joystick": "🕹️", + "jp": "🇯🇵", + "judge": "🧑‍⚖️", + "juggling_person": "🤹", + "kaaba": "🕋", + "kangaroo": "🦘", + "kazakhstan": "🇰🇿", + "kenya": "🇰🇪", + "key": "🔑", + "keyboard": "⌨️", + "keycap_ten": "🔟", + "kick_scooter": "🛴", + "kimono": "👘", + "kiribati": "🇰🇮", + "kiss": "💋", + "kissing": "😗", + "kissing_cat": "😽", + "kissing_closed_eyes": "😚", + "kissing_heart": "😘", + "kissing_smiling_eyes": "😙", + "kite": "🪁", + "kiwi_fruit": "🥝", + "kneeling_man": "🧎‍♂️", + "kneeling_person": "🧎", + "kneeling_woman": "🧎‍♀️", + "knife": "🔪", + "knot": "🪢", + "koala": "🐨", + "koko": "🈁", + "kosovo": "🇽🇰", + "kr": "🇰🇷", + "kuwait": "🇰🇼", + "kyrgyzstan": "🇰🇬", + "lab_coat": "🥼", + "label": "🏷️", + "lacrosse": "🥍", + "ladder": "🪜", + "lady_beetle": "🐞", + "lantern": "🏮", + "laos": "🇱🇦", + "large_blue_circle": "🔵", + "large_blue_diamond": "🔷", + "large_orange_diamond": "🔶", + "last_quarter_moon": "🌗", + "last_quarter_moon_with_face": "🌜", + "latin_cross": "✝️", + "latvia": "🇱🇻", + "laughing": "😆", + "leafy_green": "🥬", + "leaves": "🍃", + "lebanon": "🇱🇧", + "ledger": "📒", + "left_luggage": "🛅", + "left_right_arrow": "↔️", + "left_speech_bubble": "🗨️", + "leftwards_arrow_with_hook": "↩️", + "leg": "🦵", + "lemon": "🍋", + "leo": "♌", + "leopard": "🐆", + "lesotho": "🇱🇸", + "level_slider": "🎚️", + "liberia": "🇱🇷", + "libra": "♎", + "libya": "🇱🇾", + "liechtenstein": "🇱🇮", + "light_rail": "🚈", + "link": "🔗", + "lion": "🦁", + "lips": "👄", + "lipstick": "💄", + "lithuania": "🇱🇹", + "lizard": "🦎", + "llama": "🦙", + "lobster": "🦞", + "lock": "🔒", + "lock_with_ink_pen": "🔏", + "lollipop": "🍭", + "long_drum": "🪘", + "loop": "➿", + "lotion_bottle": "🧴", + "lotus_position": "🧘", + "lotus_position_man": "🧘‍♂️", + "lotus_position_woman": "🧘‍♀️", + "loud_sound": "🔊", + "loudspeaker": "📢", + "love_hotel": "🏩", + "love_letter": "💌", + "love_you_gesture": "🤟", + "low_brightness": "🔅", + "luggage": "🧳", + "lungs": "🫁", + "luxembourg": "🇱🇺", + "lying_face": "🤥", + "m": "Ⓜ️", + "macau": "🇲🇴", + "macedonia": "🇲🇰", + "madagascar": "🇲🇬", + "mag": "🔍", + "mag_right": "🔎", + "mage": "🧙", + "mage_man": "🧙‍♂️", + "mage_woman": "🧙‍♀️", + "magic_wand": "🪄", + "magnet": "🧲", + "mahjong": "🀄", + "mailbox": "📫", + "mailbox_closed": "📪", + "mailbox_with_mail": "📬", + "mailbox_with_no_mail": "📭", + "malawi": "🇲🇼", + "malaysia": "🇲🇾", + "maldives": "🇲🇻", + "male_detective": "🕵️‍♂️", + "male_sign": "♂️", + "mali": "🇲🇱", + "malta": "🇲🇹", + "mammoth": "🦣", + "man": "👨", + "man_artist": "👨‍🎨", + "man_astronaut": "👨‍🚀", + "man_beard": "🧔‍♂️", + "man_cartwheeling": "🤸‍♂️", + "man_cook": "👨‍🍳", + "man_dancing": "🕺", + "man_facepalming": "🤦‍♂️", + "man_factory_worker": "👨‍🏭", + "man_farmer": "👨‍🌾", + "man_feeding_baby": "👨‍🍼", + "man_firefighter": "👨‍🚒", + "man_health_worker": "👨‍⚕️", + "man_in_manual_wheelchair": "👨‍🦽", + "man_in_motorized_wheelchair": "👨‍🦼", + "man_in_tuxedo": "🤵‍♂️", + "man_judge": "👨‍⚖️", + "man_juggling": "🤹‍♂️", + "man_mechanic": "👨‍🔧", + "man_office_worker": "👨‍💼", + "man_pilot": "👨‍✈️", + "man_playing_handball": "🤾‍♂️", + "man_playing_water_polo": "🤽‍♂️", + "man_scientist": "👨‍🔬", + "man_shrugging": "🤷‍♂️", + "man_singer": "👨‍🎤", + "man_student": "👨‍🎓", + "man_teacher": "👨‍🏫", + "man_technologist": "👨‍💻", + "man_with_gua_pi_mao": "👲", + "man_with_probing_cane": "👨‍🦯", + "man_with_turban": "👳‍♂️", + "man_with_veil": "👰‍♂️", + "mandarin": "🍊", + "mango": "🥭", + "mans_shoe": "👞", + "mantelpiece_clock": "🕰️", + "manual_wheelchair": "🦽", + "maple_leaf": "🍁", + "marshall_islands": "🇲🇭", + "martial_arts_uniform": "🥋", + "martinique": "🇲🇶", + "mask": "😷", + "massage": "💆", + "massage_man": "💆‍♂️", + "massage_woman": "💆‍♀️", + "mate": "🧉", + "mauritania": "🇲🇷", + "mauritius": "🇲🇺", + "mayotte": "🇾🇹", + "meat_on_bone": "🍖", + "mechanic": "🧑‍🔧", + "mechanical_arm": "🦾", + "mechanical_leg": "🦿", + "medal_military": "🎖️", + "medal_sports": "🏅", + "medical_symbol": "⚕️", + "mega": "📣", + "melon": "🍈", + "memo": "📝", + "men_wrestling": "🤼‍♂️", + "mending_heart": "❤️‍🩹", + "menorah": "🕎", + "mens": "🚹", + "mermaid": "🧜‍♀️", + "merman": "🧜‍♂️", + "merperson": "🧜", + "metal": "🤘", + "metro": "🚇", + "mexico": "🇲🇽", + "microbe": "🦠", + "micronesia": "🇫🇲", + "microphone": "🎤", + "microscope": "🔬", + "middle_finger": "🖕", + "military_helmet": "🪖", + "milk_glass": "🥛", + "milky_way": "🌌", + "minibus": "🚐", + "minidisc": "💽", + "mirror": "🪞", + "mobile_phone_off": "📴", + "moldova": "🇲🇩", + "monaco": "🇲🇨", + "money_mouth_face": "🤑", + "money_with_wings": "💸", + "moneybag": "💰", + "mongolia": "🇲🇳", + "monkey": "🐒", + "monkey_face": "🐵", + "monocle_face": "🧐", + "monorail": "🚝", + "montenegro": "🇲🇪", + "montserrat": "🇲🇸", + "moon": "🌔", + "moon_cake": "🥮", + "morocco": "🇲🇦", + "mortar_board": "🎓", + "mosque": "🕌", + "mosquito": "🦟", + "motor_boat": "🛥️", + "motor_scooter": "🛵", + "motorcycle": "🏍️", + "motorized_wheelchair": "🦼", + "motorway": "🛣️", + "mount_fuji": "🗻", + "mountain": "⛰️", + "mountain_bicyclist": "🚵", + "mountain_biking_man": "🚵‍♂️", + "mountain_biking_woman": "🚵‍♀️", + "mountain_cableway": "🚠", + "mountain_railway": "🚞", + "mountain_snow": "🏔️", + "mouse": "🐭", + "mouse2": "🐁", + "mouse_trap": "🪤", + "movie_camera": "🎥", + "moyai": "🗿", + "mozambique": "🇲🇿", + "mrs_claus": "🤶", + "muscle": "💪", + "mushroom": "🍄", + "musical_keyboard": "🎹", + "musical_note": "🎵", + "musical_score": "🎼", + "mute": "🔇", + "mx_claus": "🧑‍🎄", + "myanmar": "🇲🇲", + "nail_care": "💅", + "name_badge": "📛", + "namibia": "🇳🇦", + "national_park": "🏞️", + "nauru": "🇳🇷", + "nauseated_face": "🤢", + "nazar_amulet": "🧿", + "necktie": "👔", + "negative_squared_cross_mark": "❎", + "nepal": "🇳🇵", + "nerd_face": "🤓", + "nesting_dolls": "🪆", + "netherlands": "🇳🇱", + "neutral_face": "😐", + "new": "🆕", + "new_caledonia": "🇳🇨", + "new_moon": "🌑", + "new_moon_with_face": "🌚", + "new_zealand": "🇳🇿", + "newspaper": "📰", + "newspaper_roll": "🗞️", + "next_track_button": "⏭️", + "ng": "🆖", + "ng_man": "🙅‍♂️", + "ng_woman": "🙅‍♀️", + "nicaragua": "🇳🇮", + "niger": "🇳🇪", + "nigeria": "🇳🇬", + "night_with_stars": "🌃", + "nine": "9️⃣", + "ninja": "🥷", + "niue": "🇳🇺", + "no_bell": "🔕", + "no_bicycles": "🚳", + "no_entry": "⛔", + "no_entry_sign": "🚫", + "no_good": "🙅", + "no_good_man": "🙅‍♂️", + "no_good_woman": "🙅‍♀️", + "no_mobile_phones": "📵", + "no_mouth": "😶", + "no_pedestrians": "🚷", + "no_smoking": "🚭", + "non-potable_water": "🚱", + "norfolk_island": "🇳🇫", + "north_korea": "🇰🇵", + "northern_mariana_islands": "🇲🇵", + "norway": "🇳🇴", + "nose": "👃", + "notebook": "📓", + "notebook_with_decorative_cover": "📔", + "notes": "🎶", + "nut_and_bolt": "🔩", + "o": "⭕", + "o2": "🅾️", + "ocean": "🌊", + "octopus": "🐙", + "oden": "🍢", + "office": "🏢", + "office_worker": "🧑‍💼", + "oil_drum": "🛢️", + "ok": "🆗", + "ok_hand": "👌", + "ok_man": "🙆‍♂️", + "ok_person": "🙆", + "ok_woman": "🙆‍♀️", + "old_key": "🗝️", + "older_adult": "🧓", + "older_man": "👴", + "older_woman": "👵", + "olive": "🫒", + "om": "🕉️", + "oman": "🇴🇲", + "on": "🔛", + "oncoming_automobile": "🚘", + "oncoming_bus": "🚍", + "oncoming_police_car": "🚔", + "oncoming_taxi": "🚖", + "one": "1️⃣", + "one_piece_swimsuit": "🩱", + "onion": "🧅", + "open_book": "📖", + "open_file_folder": "📂", + "open_hands": "👐", + "open_mouth": "😮", + "open_umbrella": "☂️", + "ophiuchus": "⛎", + "orange": "🍊", + "orange_book": "📙", + "orange_circle": "🟠", + "orange_heart": "🧡", + "orange_square": "🟧", + "orangutan": "🦧", + "orthodox_cross": "☦️", + "otter": "🦦", + "outbox_tray": "📤", + "owl": "🦉", + "ox": "🐂", + "oyster": "🦪", + "package": "📦", + "page_facing_up": "📄", + "page_with_curl": "📃", + "pager": "📟", + "paintbrush": "🖌️", + "pakistan": "🇵🇰", + "palau": "🇵🇼", + "palestinian_territories": "🇵🇸", + "palm_tree": "🌴", + "palms_up_together": "🤲", + "panama": "🇵🇦", + "pancakes": "🥞", + "panda_face": "🐼", + "paperclip": "📎", + "paperclips": "🖇️", + "papua_new_guinea": "🇵🇬", + "parachute": "🪂", + "paraguay": "🇵🇾", + "parasol_on_ground": "⛱️", + "parking": "🅿️", + "parrot": "🦜", + "part_alternation_mark": "〽️", + "partly_sunny": "⛅", + "partying_face": "🥳", + "passenger_ship": "🛳️", + "passport_control": "🛂", + "pause_button": "⏸️", + "paw_prints": "🐾", + "peace_symbol": "☮️", + "peach": "🍑", + "peacock": "🦚", + "peanuts": "🥜", + "pear": "🍐", + "pen": "🖊️", + "pencil": "📝", + "pencil2": "✏️", + "penguin": "🐧", + "pensive": "😔", + "people_holding_hands": "🧑‍🤝‍🧑", + "people_hugging": "🫂", + "performing_arts": "🎭", + "persevere": "😣", + "person_bald": "🧑‍🦲", + "person_curly_hair": "🧑‍🦱", + "person_feeding_baby": "🧑‍🍼", + "person_fencing": "🤺", + "person_in_manual_wheelchair": "🧑‍🦽", + "person_in_motorized_wheelchair": "🧑‍🦼", + "person_in_tuxedo": "🤵", + "person_red_hair": "🧑‍🦰", + "person_white_hair": "🧑‍🦳", + "person_with_probing_cane": "🧑‍🦯", + "person_with_turban": "👳", + "person_with_veil": "👰", + "peru": "🇵🇪", + "petri_dish": "🧫", + "philippines": "🇵🇭", + "phone": "☎️", + "pick": "⛏️", + "pickup_truck": "🛻", + "pie": "🥧", + "pig": "🐷", + "pig2": "🐖", + "pig_nose": "🐽", + "pill": "💊", + "pilot": "🧑‍✈️", + "pinata": "🪅", + "pinched_fingers": "🤌", + "pinching_hand": "🤏", + "pineapple": "🍍", + "ping_pong": "🏓", + "pirate_flag": "🏴‍☠️", + "pisces": "♓", + "pitcairn_islands": "🇵🇳", + "pizza": "🍕", + "placard": "🪧", + "place_of_worship": "🛐", + "plate_with_cutlery": "🍽️", + "play_or_pause_button": "⏯️", + "pleading_face": "🥺", + "plunger": "🪠", + "point_down": "👇", + "point_left": "👈", + "point_right": "👉", + "point_up": "☝️", + "point_up_2": "👆", + "poland": "🇵🇱", + "polar_bear": "🐻‍❄️", + "police_car": "🚓", + "police_officer": "👮", + "policeman": "👮‍♂️", + "policewoman": "👮‍♀️", + "poodle": "🐩", + "poop": "💩", + "popcorn": "🍿", + "portugal": "🇵🇹", + "post_office": "🏣", + "postal_horn": "📯", + "postbox": "📮", + "potable_water": "🚰", + "potato": "🥔", + "potted_plant": "🪴", + "pouch": "👝", + "poultry_leg": "🍗", + "pound": "💷", + "pout": "😡", + "pouting_cat": "😾", + "pouting_face": "🙎", + "pouting_man": "🙎‍♂️", + "pouting_woman": "🙎‍♀️", + "pray": "🙏", + "prayer_beads": "📿", + "pregnant_woman": "🤰", + "pretzel": "🥨", + "previous_track_button": "⏮️", + "prince": "🤴", + "princess": "👸", + "printer": "🖨️", + "probing_cane": "🦯", + "puerto_rico": "🇵🇷", + "punch": "👊", + "purple_circle": "🟣", + "purple_heart": "💜", + "purple_square": "🟪", + "purse": "👛", + "pushpin": "📌", + "put_litter_in_its_place": "🚮", + "qatar": "🇶🇦", + "question": "❓", + "rabbit": "🐰", + "rabbit2": "🐇", + "raccoon": "🦝", + "racehorse": "🐎", + "racing_car": "🏎️", + "radio": "📻", + "radio_button": "🔘", + "radioactive": "☢️", + "rage": "😡", + "railway_car": "🚃", + "railway_track": "🛤️", + "rainbow": "🌈", + "rainbow_flag": "🏳️‍🌈", + "raised_back_of_hand": "🤚", + "raised_eyebrow": "🤨", + "raised_hand": "✋", + "raised_hand_with_fingers_splayed": "🖐️", + "raised_hands": "🙌", + "raising_hand": "🙋", + "raising_hand_man": "🙋‍♂️", + "raising_hand_woman": "🙋‍♀️", + "ram": "🐏", + "ramen": "🍜", + "rat": "🐀", + "razor": "🪒", + "receipt": "🧾", + "record_button": "⏺️", + "recycle": "♻️", + "red_car": "🚗", + "red_circle": "🔴", + "red_envelope": "🧧", + "red_haired_man": "👨‍🦰", + "red_haired_woman": "👩‍🦰", + "red_square": "🟥", + "registered": "®️", + "relaxed": "☺️", + "relieved": "😌", + "reminder_ribbon": "🎗️", + "repeat": "🔁", + "repeat_one": "🔂", + "rescue_worker_helmet": "⛑️", + "restroom": "🚻", + "reunion": "🇷🇪", + "revolving_hearts": "💞", + "rewind": "⏪", + "rhinoceros": "🦏", + "ribbon": "🎀", + "rice": "🍚", + "rice_ball": "🍙", + "rice_cracker": "🍘", + "rice_scene": "🎑", + "right_anger_bubble": "🗯️", + "ring": "💍", + "ringed_planet": "🪐", + "robot": "🤖", + "rock": "🪨", + "rocket": "🚀", + "rofl": "🤣", + "roll_eyes": "🙄", + "roll_of_paper": "🧻", + "roller_coaster": "🎢", + "roller_skate": "🛼", + "romania": "🇷🇴", + "rooster": "🐓", + "rose": "🌹", + "rosette": "🏵️", + "rotating_light": "🚨", + "round_pushpin": "📍", + "rowboat": "🚣", + "rowing_man": "🚣‍♂️", + "rowing_woman": "🚣‍♀️", + "ru": "🇷🇺", + "rugby_football": "🏉", + "runner": "🏃", + "running": "🏃", + "running_man": "🏃‍♂️", + "running_shirt_with_sash": "🎽", + "running_woman": "🏃‍♀️", + "rwanda": "🇷🇼", + "sa": "🈂️", + "safety_pin": "🧷", + "safety_vest": "🦺", + "sagittarius": "♐", + "sailboat": "⛵", + "sake": "🍶", + "salt": "🧂", + "samoa": "🇼🇸", + "san_marino": "🇸🇲", + "sandal": "👡", + "sandwich": "🥪", + "santa": "🎅", + "sao_tome_principe": "🇸🇹", + "sari": "🥻", + "sassy_man": "💁‍♂️", + "sassy_woman": "💁‍♀️", + "satellite": "📡", + "satisfied": "😆", + "saudi_arabia": "🇸🇦", + "sauna_man": "🧖‍♂️", + "sauna_person": "🧖", + "sauna_woman": "🧖‍♀️", + "sauropod": "🦕", + "saxophone": "🎷", + "scarf": "🧣", + "school": "🏫", + "school_satchel": "🎒", + "scientist": "🧑‍🔬", + "scissors": "✂️", + "scorpion": "🦂", + "scorpius": "♏", + "scotland": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "scream": "😱", + "scream_cat": "🙀", + "screwdriver": "🪛", + "scroll": "📜", + "seal": "🦭", + "seat": "💺", + "secret": "㊙️", + "see_no_evil": "🙈", + "seedling": "🌱", + "selfie": "🤳", + "senegal": "🇸🇳", + "serbia": "🇷🇸", + "service_dog": "🐕‍🦺", + "seven": "7️⃣", + "sewing_needle": "🪡", + "seychelles": "🇸🇨", + "shallow_pan_of_food": "🥘", + "shamrock": "☘️", + "shark": "🦈", + "shaved_ice": "🍧", + "sheep": "🐑", + "shell": "🐚", + "shield": "🛡️", + "shinto_shrine": "⛩️", + "ship": "🚢", + "shirt": "👕", + "shit": "💩", + "shoe": "👞", + "shopping": "🛍️", + "shopping_cart": "🛒", + "shorts": "🩳", + "shower": "🚿", + "shrimp": "🦐", + "shrug": "🤷", + "shushing_face": "🤫", + "sierra_leone": "🇸🇱", + "signal_strength": "📶", + "singapore": "🇸🇬", + "singer": "🧑‍🎤", + "sint_maarten": "🇸🇽", + "six": "6️⃣", + "six_pointed_star": "🔯", + "skateboard": "🛹", + "ski": "🎿", + "skier": "⛷️", + "skull": "💀", + "skull_and_crossbones": "☠️", + "skunk": "🦨", + "sled": "🛷", + "sleeping": "😴", + "sleeping_bed": "🛌", + "sleepy": "😪", + "slightly_frowning_face": "🙁", + "slightly_smiling_face": "🙂", + "slot_machine": "🎰", + "sloth": "🦥", + "slovakia": "🇸🇰", + "slovenia": "🇸🇮", + "small_airplane": "🛩️", + "small_blue_diamond": "🔹", + "small_orange_diamond": "🔸", + "small_red_triangle": "🔺", + "small_red_triangle_down": "🔻", + "smile": "😄", + "smile_cat": "😸", + "smiley": "😃", + "smiley_cat": "😺", + "smiling_face_with_tear": "🥲", + "smiling_face_with_three_hearts": "🥰", + "smiling_imp": "😈", + "smirk": "😏", + "smirk_cat": "😼", + "smoking": "🚬", + "snail": "🐌", + "snake": "🐍", + "sneezing_face": "🤧", + "snowboarder": "🏂", + "snowflake": "❄️", + "snowman": "⛄", + "snowman_with_snow": "☃️", + "soap": "🧼", + "sob": "😭", + "soccer": "⚽", + "socks": "🧦", + "softball": "🥎", + "solomon_islands": "🇸🇧", + "somalia": "🇸🇴", + "soon": "🔜", + "sos": "🆘", + "sound": "🔉", + "south_africa": "🇿🇦", + "south_georgia_south_sandwich_islands": "🇬🇸", + "south_sudan": "🇸🇸", + "space_invader": "👾", + "spades": "♠️", + "spaghetti": "🍝", + "sparkle": "❇️", + "sparkler": "🎇", + "sparkles": "✨", + "sparkling_heart": "💖", + "speak_no_evil": "🙊", + "speaker": "🔈", + "speaking_head": "🗣️", + "speech_balloon": "💬", + "speedboat": "🚤", + "spider": "🕷️", + "spider_web": "🕸️", + "spiral_calendar": "🗓️", + "spiral_notepad": "🗒️", + "sponge": "🧽", + "spoon": "🥄", + "squid": "🦑", + "sri_lanka": "🇱🇰", + "st_barthelemy": "🇧🇱", + "st_helena": "🇸🇭", + "st_kitts_nevis": "🇰🇳", + "st_lucia": "🇱🇨", + "st_martin": "🇲🇫", + "st_pierre_miquelon": "🇵🇲", + "st_vincent_grenadines": "🇻🇨", + "stadium": "🏟️", + "standing_man": "🧍‍♂️", + "standing_person": "🧍", + "standing_woman": "🧍‍♀️", + "star": "⭐", + "star2": "🌟", + "star_and_crescent": "☪️", + "star_of_david": "✡️", + "star_struck": "🤩", + "stars": "🌠", + "station": "🚉", + "statue_of_liberty": "🗽", + "steam_locomotive": "🚂", + "stethoscope": "🩺", + "stew": "🍲", + "stop_button": "⏹️", + "stop_sign": "🛑", + "stopwatch": "⏱️", + "straight_ruler": "📏", + "strawberry": "🍓", + "stuck_out_tongue": "😛", + "stuck_out_tongue_closed_eyes": "😝", + "stuck_out_tongue_winking_eye": "😜", + "student": "🧑‍🎓", + "studio_microphone": "🎙️", + "stuffed_flatbread": "🥙", + "sudan": "🇸🇩", + "sun_behind_large_cloud": "🌥️", + "sun_behind_rain_cloud": "🌦️", + "sun_behind_small_cloud": "🌤️", + "sun_with_face": "🌞", + "sunflower": "🌻", + "sunglasses": "😎", + "sunny": "☀️", + "sunrise": "🌅", + "sunrise_over_mountains": "🌄", + "superhero": "🦸", + "superhero_man": "🦸‍♂️", + "superhero_woman": "🦸‍♀️", + "supervillain": "🦹", + "supervillain_man": "🦹‍♂️", + "supervillain_woman": "🦹‍♀️", + "surfer": "🏄", + "surfing_man": "🏄‍♂️", + "surfing_woman": "🏄‍♀️", + "suriname": "🇸🇷", + "sushi": "🍣", + "suspension_railway": "🚟", + "svalbard_jan_mayen": "🇸🇯", + "swan": "🦢", + "swaziland": "🇸🇿", + "sweat": "😓", + "sweat_drops": "💦", + "sweat_smile": "😅", + "sweden": "🇸🇪", + "sweet_potato": "🍠", + "swim_brief": "🩲", + "swimmer": "🏊", + "swimming_man": "🏊‍♂️", + "swimming_woman": "🏊‍♀️", + "switzerland": "🇨🇭", + "symbols": "🔣", + "synagogue": "🕍", + "syria": "🇸🇾", + "syringe": "💉", + "t-rex": "🦖", + "taco": "🌮", + "tada": "🎉", + "taiwan": "🇹🇼", + "tajikistan": "🇹🇯", + "takeout_box": "🥡", + "tamale": "🫔", + "tanabata_tree": "🎋", + "tangerine": "🍊", + "tanzania": "🇹🇿", + "taurus": "♉", + "taxi": "🚕", + "tea": "🍵", + "teacher": "🧑‍🏫", + "teapot": "🫖", + "technologist": "🧑‍💻", + "teddy_bear": "🧸", + "telephone": "☎️", + "telephone_receiver": "📞", + "telescope": "🔭", + "tennis": "🎾", + "tent": "⛺", + "test_tube": "🧪", + "thailand": "🇹🇭", + "thermometer": "🌡️", + "thinking": "🤔", + "thong_sandal": "🩴", + "thought_balloon": "💭", + "thread": "🧵", + "three": "3️⃣", + "thumbsdown": "👎", + "thumbsup": "👍", + "ticket": "🎫", + "tickets": "🎟️", + "tiger": "🐯", + "tiger2": "🐅", + "timer_clock": "⏲️", + "timor_leste": "🇹🇱", + "tipping_hand_man": "💁‍♂️", + "tipping_hand_person": "💁", + "tipping_hand_woman": "💁‍♀️", + "tired_face": "😫", + "tm": "™️", + "togo": "🇹🇬", + "toilet": "🚽", + "tokelau": "🇹🇰", + "tokyo_tower": "🗼", + "tomato": "🍅", + "tonga": "🇹🇴", + "tongue": "👅", + "toolbox": "🧰", + "tooth": "🦷", + "toothbrush": "🪥", + "top": "🔝", + "tophat": "🎩", + "tornado": "🌪️", + "tr": "🇹🇷", + "trackball": "🖲️", + "tractor": "🚜", + "traffic_light": "🚥", + "train": "🚋", + "train2": "🚆", + "tram": "🚊", + "transgender_flag": "🏳️‍⚧️", + "transgender_symbol": "⚧️", + "triangular_flag_on_post": "🚩", + "triangular_ruler": "📐", + "trident": "🔱", + "trinidad_tobago": "🇹🇹", + "tristan_da_cunha": "🇹🇦", + "triumph": "😤", + "trolleybus": "🚎", + "trophy": "🏆", + "tropical_drink": "🍹", + "tropical_fish": "🐠", + "truck": "🚚", + "trumpet": "🎺", + "tshirt": "👕", + "tulip": "🌷", + "tumbler_glass": "🥃", + "tunisia": "🇹🇳", + "turkey": "🦃", + "turkmenistan": "🇹🇲", + "turks_caicos_islands": "🇹🇨", + "turtle": "🐢", + "tuvalu": "🇹🇻", + "tv": "📺", + "twisted_rightwards_arrows": "🔀", + "two": "2️⃣", + "two_hearts": "💕", + "two_men_holding_hands": "👬", + "two_women_holding_hands": "👭", + "u5272": "🈹", + "u5408": "🈴", + "u55b6": "🈺", + "u6307": "🈯", + "u6708": "🈷️", + "u6709": "🈶", + "u6e80": "🈵", + "u7121": "🈚", + "u7533": "🈸", + "u7981": "🈲", + "u7a7a": "🈳", + "uganda": "🇺🇬", + "uk": "🇬🇧", + "ukraine": "🇺🇦", + "umbrella": "☔", + "unamused": "😒", + "underage": "🔞", + "unicorn": "🦄", + "united_arab_emirates": "🇦🇪", + "united_nations": "🇺🇳", + "unlock": "🔓", + "up": "🆙", + "upside_down_face": "🙃", + "uruguay": "🇺🇾", + "us": "🇺🇸", + "us_outlying_islands": "🇺🇲", + "us_virgin_islands": "🇻🇮", + "uzbekistan": "🇺🇿", + "v": "✌️", + "vampire": "🧛", + "vampire_man": "🧛‍♂️", + "vampire_woman": "🧛‍♀️", + "vanuatu": "🇻🇺", + "vatican_city": "🇻🇦", + "venezuela": "🇻🇪", + "vertical_traffic_light": "🚦", + "vhs": "📼", + "vibration_mode": "📳", + "video_camera": "📹", + "video_game": "🎮", + "vietnam": "🇻🇳", + "violin": "🎻", + "virgo": "♍", + "volcano": "🌋", + "volleyball": "🏐", + "vomiting_face": "🤮", + "vs": "🆚", + "vulcan_salute": "🖖", + "waffle": "🧇", + "wales": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + "walking": "🚶", + "walking_man": "🚶‍♂️", + "walking_woman": "🚶‍♀️", + "wallis_futuna": "🇼🇫", + "waning_crescent_moon": "🌘", + "waning_gibbous_moon": "🌖", + "warning": "⚠️", + "wastebasket": "🗑️", + "watch": "⌚", + "water_buffalo": "🐃", + "water_polo": "🤽", + "watermelon": "🍉", + "wave": "👋", + "wavy_dash": "〰️", + "waxing_crescent_moon": "🌒", + "waxing_gibbous_moon": "🌔", + "wc": "🚾", + "weary": "😩", + "wedding": "💒", + "weight_lifting": "🏋️", + "weight_lifting_man": "🏋️‍♂️", + "weight_lifting_woman": "🏋️‍♀️", + "western_sahara": "🇪🇭", + "whale": "🐳", + "whale2": "🐋", + "wheel_of_dharma": "☸️", + "wheelchair": "♿", + "white_check_mark": "✅", + "white_circle": "⚪", + "white_flag": "🏳️", + "white_flower": "💮", + "white_haired_man": "👨‍🦳", + "white_haired_woman": "👩‍🦳", + "white_heart": "🤍", + "white_large_square": "⬜", + "white_medium_small_square": "◽", + "white_medium_square": "◻️", + "white_small_square": "▫️", + "white_square_button": "🔳", + "wilted_flower": "🥀", + "wind_chime": "🎐", + "wind_face": "🌬️", + "window": "🪟", + "wine_glass": "🍷", + "wink": "😉", + "wolf": "🐺", + "woman": "👩", + "woman_artist": "👩‍🎨", + "woman_astronaut": "👩‍🚀", + "woman_beard": "🧔‍♀️", + "woman_cartwheeling": "🤸‍♀️", + "woman_cook": "👩‍🍳", + "woman_dancing": "💃", + "woman_facepalming": "🤦‍♀️", + "woman_factory_worker": "👩‍🏭", + "woman_farmer": "👩‍🌾", + "woman_feeding_baby": "👩‍🍼", + "woman_firefighter": "👩‍🚒", + "woman_health_worker": "👩‍⚕️", + "woman_in_manual_wheelchair": "👩‍🦽", + "woman_in_motorized_wheelchair": "👩‍🦼", + "woman_in_tuxedo": "🤵‍♀️", + "woman_judge": "👩‍⚖️", + "woman_juggling": "🤹‍♀️", + "woman_mechanic": "👩‍🔧", + "woman_office_worker": "👩‍💼", + "woman_pilot": "👩‍✈️", + "woman_playing_handball": "🤾‍♀️", + "woman_playing_water_polo": "🤽‍♀️", + "woman_scientist": "👩‍🔬", + "woman_shrugging": "🤷‍♀️", + "woman_singer": "👩‍🎤", + "woman_student": "👩‍🎓", + "woman_teacher": "👩‍🏫", + "woman_technologist": "👩‍💻", + "woman_with_headscarf": "🧕", + "woman_with_probing_cane": "👩‍🦯", + "woman_with_turban": "👳‍♀️", + "woman_with_veil": "👰‍♀️", + "womans_clothes": "👚", + "womans_hat": "👒", + "women_wrestling": "🤼‍♀️", + "womens": "🚺", + "wood": "🪵", + "woozy_face": "🥴", + "world_map": "🗺️", + "worm": "🪱", + "worried": "😟", + "wrench": "🔧", + "wrestling": "🤼", + "writing_hand": "✍️", + "x": "❌", + "yarn": "🧶", + "yawning_face": "🥱", + "yellow_circle": "🟡", + "yellow_heart": "💛", + "yellow_square": "🟨", + "yemen": "🇾🇪", + "yen": "💴", + "yin_yang": "☯️", + "yo_yo": "🪀", + "yum": "😋", + "zambia": "🇿🇲", + "zany_face": "🤪", + "zap": "⚡", + "zebra": "🦓", + "zero": "0️⃣", + "zimbabwe": "🇿🇼", + "zipper_mouth_face": "🤐", + "zombie": "🧟", + "zombie_man": "🧟‍♂️", + "zombie_woman": "🧟‍♀️", + "zzz": "💤" +} \ No newline at end of file diff --git a/ntfy-daemon/src/lib.rs b/ntfy-daemon/src/lib.rs new file mode 100644 index 0000000..63682aa --- /dev/null +++ b/ntfy-daemon/src/lib.rs @@ -0,0 +1,24 @@ +pub mod message_repo; +pub mod models; +pub mod ntfy_proxy; +pub mod retry; +pub mod system_client; +pub mod ntfy_capnp { + include!(concat!(env!("OUT_DIR"), "/src/ntfy_capnp.rs")); +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("topic {0} must not be empty and must contain only alphanumeric characters and _ (underscore)")] + InvalidTopic(String), + #[error("duplicate message")] + DuplicateMessage, + #[error("can't parse the minimum set of required fields from the message {0}")] + InvalidMinMessage(String, #[source] serde_json::Error), + #[error("can't parse the complete message {0}")] + InvalidMessage(String, #[source] serde_json::Error), + #[error("database error")] + Db(#[from] rusqlite::Error), + #[error("subscription not found while {0}")] + SubscriptionNotFound(String), +} diff --git a/ntfy-daemon/src/message_repo/migrations/00.sql b/ntfy-daemon/src/message_repo/migrations/00.sql new file mode 100644 index 0000000..335c7f7 --- /dev/null +++ b/ntfy-daemon/src/message_repo/migrations/00.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS server ( + id INTEGER PRIMARY KEY, + endpoint TEXT NOT NULL UNIQUE, + timeout INTEGER +); + +CREATE TABLE IF NOT EXISTS subscription ( + topic TEXT, + display_name TEXT, + muted INTEGER NOT NULL DEFAULT 0, + server INTEGER REFERENCES server(id), + archived INTEGER NOT NULL DEFAULT 0, + reserved INTEGER NOT NULL DEFAULT 0, + read_until INTEGER NOT NULL DEFAULT 0, + symbolic_icon TEXT, + PRIMARY KEY (server, topic) +); + +CREATE TABLE IF NOT EXISTS message ( + server INTEGER, + data TEXT NOT NULL, + topic TEXT AS (data ->> '$.topic'), -- For the FOREIGN KEY constraint + FOREIGN KEY (server, topic) REFERENCES subscription(server, topic) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS message_by_time ON message (data ->> '$.time'); +-- I can't put a JSON expression inside a UNIQUE constraint, +-- but I can do it on a UNIQUE INDEX +CREATE UNIQUE INDEX IF NOT EXISTS server_and_message_id ON message (server, data ->> '$.id'); diff --git a/ntfy-daemon/src/message_repo/mod.rs b/ntfy-daemon/src/message_repo/mod.rs new file mode 100644 index 0000000..edbdeb2 --- /dev/null +++ b/ntfy-daemon/src/message_repo/mod.rs @@ -0,0 +1,206 @@ +use std::{cell::RefCell, rc::Rc}; + +use rusqlite::{params, Connection, Result}; +use tracing::info; + +use crate::models; +use crate::Error; + +#[derive(Clone, Debug)] +pub struct Db { + conn: Rc>, +} + +impl Db { + pub fn connect(path: &str) -> Result { + let mut this = Self { + conn: Rc::new(RefCell::new(Connection::open(path)?)), + }; + { + this.conn.borrow().execute_batch( + "PRAGMA foreign_keys = ON; + PRAGMA journal_mode = wal;", + )?; + } + this.migrate()?; + Ok(this) + } + fn migrate(&mut self) -> Result<()> { + { + self.conn + .borrow() + .execute_batch(include_str!("./migrations/00.sql"))? + }; + Ok(()) + } + fn get_or_insert_server(&mut self, server: &str) -> Result { + let mut conn = self.conn.borrow_mut(); + let tx = conn.transaction()?; + let mut res = tx.query_row( + "SELECT id + FROM server + WHERE endpoint = ?1", + params![server,], + |row| { + let id: i64 = row.get(0)?; + Ok(id) + }, + ); + if let Err(rusqlite::Error::QueryReturnedNoRows) = res { + tx.execute( + "INSERT INTO server (id, endpoint) VALUES (NULL, ?1)", + params![server,], + )?; + res = Ok(tx.last_insert_rowid()); + } + tx.commit()?; + res + } + pub fn insert_message(&mut self, server: &str, json_data: &str) -> Result<(), Error> { + let server_id = self.get_or_insert_server(server)?; + let res = self.conn.borrow().execute( + "INSERT INTO message (server, data) VALUES (?1, ?2)", + params![server_id, json_data], + ); + match res { + Err(rusqlite::Error::SqliteFailure(_, Some(text))) + if text.starts_with("UNIQUE constraint failed") => + { + Err(Error::DuplicateMessage) + } + Err(e) => Err(Error::Db(e)), + Ok(_) => Ok(()), + } + } + pub fn list_messages( + &self, + server: &str, + topic: &str, + since: u64, + ) -> Result, rusqlite::Error> { + let conn = self.conn.borrow(); + let mut stmt = conn.prepare( + " + SELECT data + FROM subscription sub + JOIN server s ON sub.server = s.id + JOIN message m ON m.server = sub.server AND m.topic = sub.topic + WHERE s.endpoint = ?1 AND m.topic = ?2 AND m.data ->> 'time' >= ?3 + ORDER BY m.data ->> 'time' + ", + )?; + let msgs: Result, _> = stmt + .query_map(params![server, topic, since], |row| Ok(row.get(0)?))? + .collect(); + Ok(msgs?) + } + pub fn insert_subscription(&mut self, sub: models::Subscription) -> Result<(), Error> { + let server_id = self.get_or_insert_server(&sub.server)?; + self.conn.borrow().execute( + "INSERT INTO subscription (server, topic, display_name, reserved, muted, archived) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![ + server_id, + sub.topic, + sub.display_name, + sub.reserved, + sub.muted, + sub.archived + ], + )?; + Ok(()) + } + pub fn remove_subscription(&mut self, server: &str, topic: &str) -> Result<(), Error> { + let server_id = self.get_or_insert_server(server)?; + let res = self.conn.borrow().execute( + "DELETE FROM subscription + WHERE server = ?1 AND topic = ?2", + params![server_id, topic], + )?; + if res <= 0 { + return Err(Error::SubscriptionNotFound("removing subscription".into())); + } + Ok(()) + } + pub fn list_subscriptions(&mut self) -> Result, Error> { + let conn = self.conn.borrow(); + let mut stmt = conn.prepare( + "SELECT server.endpoint, sub.topic, sub.display_name, sub.reserved, sub.muted, sub.archived, sub.symbolic_icon, sub.read_until + FROM subscription sub + JOIN server ON server.id = sub.server + ORDER BY server.endpoint, sub.display_name, sub.topic + ", + )?; + let rows = stmt.query_map(params![], |row| { + Ok(models::Subscription { + server: row.get(0)?, + topic: row.get(1)?, + display_name: row.get(2)?, + reserved: row.get(3)?, + muted: row.get(4)?, + archived: row.get(5)?, + symbolic_icon: row.get(6)?, + read_until: row.get(7)?, + }) + })?; + let subs: Result, rusqlite::Error> = rows.collect(); + Ok(subs?) + } + + pub fn update_subscription(&mut self, sub: models::Subscription) -> Result<(), Error> { + let server_id = self.get_or_insert_server(&sub.server)?; + let res = self.conn.borrow().execute( + "UPDATE subscription + SET display_name = ?1, reserved = ?2, muted = ?3, archived = ?4, read_until = ?5 + WHERE server = ?6 AND topic = ?7", + params![ + sub.display_name, + sub.reserved, + sub.muted, + sub.archived, + sub.read_until, + server_id, + sub.topic, + ], + )?; + if res <= 0 { + return Err(Error::SubscriptionNotFound("updating subscription".into())); + } + info!(info = ?sub, "stored subscription info"); + Ok(()) + } + + pub fn update_read_until( + &mut self, + server: &str, + topic: &str, + value: u64, + ) -> Result<(), Error> { + let server_id = self.get_or_insert_server(server).unwrap(); + let conn = self.conn.borrow(); + let res = conn.execute( + "UPDATE subscription + SET read_until = ?3 + WHERE topic = ?2 AND server = ?1 + ", + params![server_id, topic, value], + )?; + if res <= 0 { + return Err(Error::SubscriptionNotFound("updating read_until".into())); + } + Ok(()) + } + pub fn delete_messages(&mut self, server: &str, topic: &str) -> Result<(), Error> { + let server_id = self.get_or_insert_server(server).unwrap(); + let conn = self.conn.borrow(); + let res = conn.execute( + "DELETE FROM message + WHERE topic = ?2 AND server = ?1 + ", + params![server_id, topic], + )?; + if res <= 0 { + return Err(Error::SubscriptionNotFound("deleting messages".into())); + } + Ok(()) + } +} diff --git a/ntfy-daemon/src/models.rs b/ntfy-daemon/src/models.rs new file mode 100644 index 0000000..26d98cd --- /dev/null +++ b/ntfy-daemon/src/models.rs @@ -0,0 +1,263 @@ +use std::collections::HashMap; +use std::sync::OnceLock; + +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use crate::Error; + +static EMOJI_MAP: OnceLock> = OnceLock::new(); + +fn emoji_map() -> &'static HashMap { + EMOJI_MAP.get_or_init(move || { + serde_json::from_str(include_str!("../data/mailer_emoji_map.json")).unwrap() + }) +} + +fn validate_topic(topic: &str) -> Result<&str, Error> { + let re = Regex::new(r"^([A-z]|[0-9]|_)+$").unwrap(); + if re.is_match(topic) { + Ok(topic) + } else { + Err(Error::InvalidTopic(topic.to_string())) + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct Message { + pub topic: String, + pub message: Option, + pub time: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub priority: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub attach: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delay: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub call: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub actions: Vec, +} + +impl Message { + fn extend_with_emojis(&self, text: &mut String) { + // Add emojis + for t in &self.tags { + if let Some(emoji) = emoji_map().get(t) { + text.push_str(emoji); + } + } + } + pub fn display_title(&self) -> Option { + self.title.as_ref().map(|title| { + let mut title_text = String::new(); + self.extend_with_emojis(&mut title_text); + + if !title_text.is_empty() { + title_text.push(' '); + } + + title_text.push_str(title); + title_text + }) + } + + pub fn display_message(&self) -> Option { + self.message.as_ref().map(|message| { + let mut out = String::new(); + if self.title.is_none() { + self.extend_with_emojis(&mut out); + } + if !out.is_empty() { + out.push(' '); + } + + out.push_str(message); + out + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MinMessage { + pub id: String, + pub topic: String, + pub time: u64, +} + +#[derive(Clone, Debug)] +pub struct Subscription { + pub server: String, + pub topic: String, + pub display_name: String, + pub muted: bool, + pub archived: bool, + pub reserved: bool, + pub symbolic_icon: Option, + pub read_until: u64, +} + +impl Subscription { + pub fn build_url(server: &str, topic: &str, since: u64) -> anyhow::Result { + let mut url = url::Url::parse(server)?; + url.path_segments_mut() + .map_err(|_| anyhow::anyhow!("url can't be base"))? + .push(&topic) + .push("json"); + url.query_pairs_mut() + .append_pair("since", &since.to_string()); + Ok(url) + } + pub fn validate(self) -> anyhow::Result { + validate_topic(&self.topic)?; + Self::build_url(&self.server, &self.topic, 0)?; + Ok(self) + } + pub fn builder(server: String, topic: String) -> SubscriptionBuilder { + SubscriptionBuilder::new(server, topic) + } +} + +#[derive(Clone)] +pub struct SubscriptionBuilder { + server: String, + topic: String, + muted: bool, + archived: bool, + reserved: bool, + symbolic_icon: Option, + display_name: String, +} + +impl SubscriptionBuilder { + pub fn new(server: String, topic: String) -> Self { + Self { + server, + topic, + muted: false, + archived: false, + reserved: false, + symbolic_icon: None, + display_name: String::new(), + } + } + + pub fn muted(mut self, muted: bool) -> Self { + self.muted = muted; + self + } + + pub fn archived(mut self, archived: bool) -> Self { + self.archived = archived; + self + } + + pub fn reserved(mut self, reserved: bool) -> Self { + self.reserved = reserved; + self + } + + pub fn symbolic_icon(mut self, symbolic_icon: Option) -> Self { + self.symbolic_icon = symbolic_icon; + self + } + + pub fn display_name(mut self, display_name: String) -> Self { + self.display_name = display_name; + self + } + + pub fn build(self) -> anyhow::Result { + let res = Subscription { + server: self.server, + topic: self.topic, + muted: self.muted, + archived: self.archived, + reserved: self.reserved, + symbolic_icon: self.symbolic_icon, + display_name: self.display_name, + read_until: 0, + }; + res.validate() + } +} + +fn default_method() -> String { + "POST".to_string() +} +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "action")] +pub enum Action { + #[serde(rename = "view")] + View { + label: String, + url: String, + #[serde(default)] + clear: bool, + }, + #[serde(rename = "http")] + Http { + label: String, + url: String, + #[serde(default = "default_method")] + method: String, + #[serde(default)] + headers: HashMap, + #[serde(default)] + body: String, + #[serde(default)] + clear: bool, + }, + #[serde(rename = "broadcast")] + Broadcast { + label: String, + intent: Option, + #[serde(default)] + extras: HashMap, + #[serde(default)] + clear: bool, + }, +} + +#[derive(Debug, PartialEq, Copy, Clone, Default)] +pub enum Status { + #[default] + Down, + Degraded, + Up, +} + +impl From for Status { + fn from(item: u8) -> Self { + match item { + 0 => Status::Down, + 1 => Status::Degraded, + 2 => Status::Up, + _ => Status::Down, + } + } +} + +impl From for u8 { + fn from(item: Status) -> Self { + match item { + Status::Down => 0, + Status::Degraded => 1, + Status::Up => 2, + } + } +} diff --git a/ntfy-daemon/src/ntfy.capnp b/ntfy-daemon/src/ntfy.capnp new file mode 100644 index 0000000..bab9bd7 --- /dev/null +++ b/ntfy-daemon/src/ntfy.capnp @@ -0,0 +1,47 @@ +@0x9663f4dd604afa35; + +enum Status { + down @0; + degraded @1; + up @2; +} + +interface WatchHandle {} + +interface OutputChannel { + sendMessage @0 (message: Text); + sendStatus @1 (status: Status); + done @2 (); +} + +interface NtfyProxy { + getServer @0 () -> (server: Text); + watch @1 (topic: Text, watcher: OutputChannel, since: UInt64) -> (handle: WatchHandle); + publish @2 (message: Text); +} + +struct SubscriptionInfo { + server @0 :Text; + topic @1 :Text; + displayName @2 :Text; + muted @3 :Bool; + readUntil @4 :UInt64; +} + +interface Subscription { + watch @0 (watcher: OutputChannel, since: UInt64) -> (handle: WatchHandle); + publish @1 (message: Text); + + getInfo @2 () -> SubscriptionInfo; + updateInfo @3 (value: SubscriptionInfo); + updateReadUntil @4 (value: UInt64); + + clearNotifications @5 (); +} + + +interface SystemNotifier { + subscribe @0 (server: Text, topic: Text) -> (subscription: Subscription); + unsubscribe @1 (server: Text, topic: Text); + listSubscriptions @2 () -> (list: List(Subscription)); +} diff --git a/ntfy-daemon/src/ntfy_proxy.rs b/ntfy-daemon/src/ntfy_proxy.rs new file mode 100644 index 0000000..5affe37 --- /dev/null +++ b/ntfy-daemon/src/ntfy_proxy.rs @@ -0,0 +1,355 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::ops::ControlFlow; +use std::rc::{Rc, Weak}; +use std::time::Duration; + +use ashpd::desktop::network_monitor::NetworkMonitor; +use capnp::capability::Promise; +use capnp_rpc::pry; +use futures::future::RemoteHandle; +use futures::prelude::*; +use reqwest::header::HeaderValue; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncBufReadExt; +use tokio::sync::mpsc; +use tokio_stream::wrappers::LinesStream; +use tracing::{debug, error, info, instrument, Instrument}; + +use crate::{ + models, + ntfy_capnp::{ntfy_proxy, output_channel, watch_handle, Status}, + Error, +}; + +const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15); +const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(240); // 4 minutes + +static GLOBAL_MONITOR: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "event")] +pub enum Event { + #[serde(rename = "open")] + Open { + id: String, + time: usize, + expires: Option, + topic: String, + }, + #[serde(rename = "message")] + Message { + id: String, + expires: Option, + #[serde(flatten)] + message: models::Message, + }, + #[serde(rename = "keepalive")] + KeepAlive { + id: String, + time: usize, + expires: Option, + topic: String, + }, +} + +fn build_client() -> anyhow::Result { + Ok(reqwest::Client::builder() + .connect_timeout(CONNECT_TIMEOUT) + .pool_idle_timeout(TIMEOUT) + // rustls is used because HTTP 2 isn't discovered with native-tls. + // HTTP 2 is required to multiplex multiple requests over a single connection. + // You can check that the app is using a single connection to a server by doing + // ``` + // ping ntfy.sh # to get the ip address + // netstat | grep $ip + // ``` + .use_rustls_tls() + .build()?) +} +fn topic_request(endpoint: &str, topic: &str, since: u64) -> anyhow::Result { + let url = models::Subscription::build_url(endpoint, topic, since)?; + let mut req = reqwest::Request::new(reqwest::Method::GET, url); + let headers = req.headers_mut(); + headers.append( + "Content-Type", + HeaderValue::from_static("application/x-ndjson"), + ); + headers.append("Transfer-Encoding", HeaderValue::from_static("chunked")); + Ok(req) +} + +async fn response_lines( + res: impl tokio::io::AsyncBufRead, +) -> Result>, reqwest::Error> { + let lines = LinesStream::new(res.lines()); + Ok(lines) +} + +pub enum BroadcasterEvent { + Stop, + Restart, +} + +struct TopicListener { + endpoint: String, + topic: String, + status: Status, + output_channel: output_channel::Client, + since: u64, + client: reqwest::Client, +} + +impl TopicListener { + fn new( + client: reqwest::Client, + endpoint: String, + topic: String, + since: u64, + output_channel: output_channel::Client, + ) -> anyhow::Result>> { + let (tx, mut rx) = mpsc::channel(8); + let mut this = Self { + endpoint, + topic, + status: Status::Down, + output_channel, + since, + client, + }; + + tokio::task::spawn_local(async move { + loop { + tokio::select! { + _ = this.run_supervised_loop().instrument(tracing::debug_span!("run_supervised_loop")) => {}, + res = rx.recv() => match res { + Some(ControlFlow::Continue(_)) => {} + None | Some(ControlFlow::Break(_)) => { + break; + } + } + } + } + }); + Ok(tx) + } + + fn send_current_status(&mut self) -> impl Future> { + let mut req = self.output_channel.send_status_request(); + req.get().set_status(self.status); + async move { + req.send().promise.await?; + Ok(()) + } + } + + #[instrument(skip_all)] + async fn recv_and_forward(&mut self) -> anyhow::Result<()> { + let req = topic_request(&self.endpoint, &self.topic, self.since)?; + let res = self.client.execute(req).await?; + let reader = tokio_util::io::StreamReader::new( + res.bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())), + ); + let stream = response_lines(reader).await?; + tokio::pin!(stream); + self.status = Status::Up; + self.send_current_status().await.unwrap(); + info!(topic = %&self.topic, "listening"); + while let Some(msg) = stream.next().await { + let msg = msg?; + + let min_msg = serde_json::from_str::(&msg) + .map_err(|e| Error::InvalidMinMessage(msg.to_string(), e))?; + self.since = min_msg.time.max(self.since); + + let event = serde_json::from_str(&msg) + .map_err(|e| Error::InvalidMessage(msg.to_string(), e))?; + + match event { + Event::Message { .. } => { + debug!("message event"); + let mut req = self.output_channel.send_message_request(); + req.get().set_message(&msg); + req.send().promise.await?; + } + Event::KeepAlive { .. } => { + debug!("keepalive event"); + } + Event::Open { .. } => { + debug!("open event"); + } + } + } + + Ok(()) + } + async fn run_supervised_loop(&mut self) { + let retrier = || { + crate::retry::WaitExponentialRandom::builder() + .min(Duration::from_secs(1)) + .max(Duration::from_secs(60 * 10)) + .build() + }; + let mut retry = retrier(); + loop { + let start_time = std::time::Instant::now(); + if let Err(e) = self.recv_and_forward().await { + let uptime = std::time::Instant::now().duration_since(start_time); + // Reset retry delay to minimum if uptime was decent enough + if uptime > Duration::from_secs(60 * 4) { + retry = retrier(); + } + error!(error = ?e); + self.status = Status::Degraded; + self.send_current_status().await.unwrap(); + info!(delay = ?retry.next_delay(), "restarting"); + retry.wait().await; + } else { + break; + } + } + } +} + +struct WatcherImpl { + topic: String, + all_topics: Weak>>>>, +} +impl Drop for WatcherImpl { + fn drop(&mut self) { + if let Some(m) = self.all_topics.upgrade() { + debug!("Dropped WatcherImpl"); + let mut m = m.borrow_mut(); + let tx = m[&self.topic].clone(); + tokio::task::spawn_local(async move { + tx.send(ControlFlow::Break(())).await.unwrap(); + }); + m.remove(&self.topic); + } + } +} + +impl watch_handle::Server for WatcherImpl {} + +// This is a proxy to the actual ntfy server. After a network issue, this will reconnect to the +// server and re-establish all watches. +pub struct NtfyProxyImpl { + endpoint: String, + watching: Rc>>>>, + client: reqwest::Client, + _monitor_task: RemoteHandle<()>, +} + +impl NtfyProxyImpl { + pub fn new(endpoint: String) -> NtfyProxyImpl { + let watching = Rc::new(RefCell::new( + HashMap::>>::new(), + )); + let watching_clone = Rc::downgrade(&watching); + + let (f, handle) = async move { + let mut prev_available = false; + + let monitor = GLOBAL_MONITOR + .get_or_init(|| async move { NetworkMonitor::new().await.unwrap() }) + .await; + while let Ok(_) = monitor.receive_changed().await { + let available = monitor.is_available().await.unwrap(); + if available && !prev_available { + info!("Refreshed"); + if let Some(ws) = watching_clone.upgrade() { + for (_, w) in ws.borrow().iter() { + w.send(ControlFlow::Continue(())).await.unwrap(); + } + } + } + prev_available = available; + } + } + .remote_handle(); + tokio::task::spawn_local(f); + NtfyProxyImpl { + endpoint, + watching: watching.clone(), + client: build_client().unwrap(), + _monitor_task: handle, + } + } + + fn _watch( + &mut self, + topic: String, + watcher: output_channel::Client, + since: u64, + ) -> anyhow::Result { + if !{ self.watching.borrow().contains_key(&topic) } { + self.watching.borrow_mut().insert( + topic.clone(), + TopicListener::new( + self.client.clone(), + self.endpoint.clone(), + topic.clone(), + since, + watcher, + )?, + ); + } + Ok(capnp_rpc::new_client(WatcherImpl { + topic, + all_topics: Rc::downgrade(&self.watching), + })) + } + fn _send_msg<'a>( + &'a mut self, + msg: &'a models::Message, + ) -> impl Future> { + let client = reqwest::Client::new(); + + let json = serde_json::to_string(&msg).unwrap(); + let req = client.post(&self.endpoint).body(json.clone()); + + async move { + info!(json = ?json, "sending message"); + let res = req.send().await; + match res { + Err(e) => Err(capnp::Error::failed(e.to_string())), + Ok(res) => { + res.error_for_status() + .map_err(|e| capnp::Error::failed(e.to_string()))?; + Ok(()) + } + } + } + } +} +impl ntfy_proxy::Server for NtfyProxyImpl { + fn publish( + &mut self, + params: ntfy_proxy::PublishParams, + _results: ntfy_proxy::PublishResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let params = params.get(); + let message = pry!(pry!(params).get_message()); + let message: models::Message = serde_json::from_str(message).unwrap(); + let res = self._send_msg(&message); + Promise::from_future(async move { + res.await.map_err(|e| capnp::Error::failed(e.to_string()))?; + Ok(()) + }) + } + fn watch( + &mut self, + params: ntfy_proxy::WatchParams, + mut results: ntfy_proxy::WatchResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let topic = pry!(pry!(params.get()).get_topic()); + let watcher = pry!(pry!(params.get()).get_watcher()); + let since = pry!(params.get()).get_since(); + let handle = pry!(self + ._watch(topic.to_owned(), watcher, since.to_owned()) + .map_err(|e| capnp::Error::failed(e.to_string()))); + results.get().set_handle(handle); + Promise::ok(()) + } +} diff --git a/ntfy-daemon/src/retry.rs b/ntfy-daemon/src/retry.rs new file mode 100644 index 0000000..0126a35 --- /dev/null +++ b/ntfy-daemon/src/retry.rs @@ -0,0 +1,56 @@ +use std::cmp; +use std::time::Duration; + +use rand::prelude::*; +use tokio::time::sleep; + +pub struct WaitExponentialRandom { + min: Duration, + max: Duration, + i: u64, + multiplier: u64, +} +pub struct WaitExponentialRandomBuilder { + inner: WaitExponentialRandom, +} + +impl WaitExponentialRandomBuilder { + pub fn build(self) -> WaitExponentialRandom { + self.inner + } + pub fn min(mut self, duration: Duration) -> Self { + self.inner.min = duration; + self + } + pub fn max(mut self, duration: Duration) -> Self { + self.inner.max = duration; + self + } + pub fn multiplier(mut self, mul: u64) -> Self { + self.inner.multiplier = mul; + self + } +} + +impl WaitExponentialRandom { + pub fn builder() -> WaitExponentialRandomBuilder { + WaitExponentialRandomBuilder { + inner: WaitExponentialRandom { + min: Duration::ZERO, + max: Duration::MAX, + i: 0, + multiplier: 1, + }, + } + } + pub fn next_delay(&self) -> Duration { + let secs = (1 << self.i) * self.multiplier; + let secs = rand::thread_rng().gen_range(self.min.as_secs()..=secs); + let dur = Duration::from_secs(secs); + cmp::min(cmp::max(dur, self.min), self.max) + } + pub async fn wait(&mut self) { + sleep(self.next_delay()).await; + self.i += 1; + } +} diff --git a/ntfy-daemon/src/system_client.rs b/ntfy-daemon/src/system_client.rs new file mode 100644 index 0000000..db269b1 --- /dev/null +++ b/ntfy-daemon/src/system_client.rs @@ -0,0 +1,504 @@ +use std::cell::OnceCell; +use std::cell::{Cell, RefCell}; +use std::rc::{Rc, Weak}; +use std::time::Duration; +use std::{collections::HashMap, hash::Hash}; + +use ashpd::desktop::notification::{Notification, NotificationProxy}; +use capnp::capability::Promise; +use capnp_rpc::{pry, rpc_twoparty_capnp, twoparty, RpcSystem}; +use futures::future::join_all; +use futures::prelude::*; +use generational_arena::Arena; +use tokio::net::UnixListener; +use tracing::{error, info, warn}; + +use crate::models::Message; +use crate::Error; +use crate::{ + message_repo::Db, + models::{self, MinMessage}, + ntfy_capnp::ntfy_proxy, + ntfy_capnp::{output_channel, subscription, system_notifier, watch_handle, Status}, + ntfy_proxy::NtfyProxyImpl, +}; + +const MESSAGE_THROTTLE: Duration = Duration::from_millis(150); + +impl From for capnp::Error { + fn from(value: Error) -> Self { + capnp::Error::failed(format!("{:?}", value)) + } +} + +pub struct NotifyForwarder { + model: Rc>, + db: Db, + watching: Weak>>, + status: Rc>, +} +impl NotifyForwarder { + pub fn new( + model: Rc>, + db: Db, + watching: Weak>>, + status: Rc>, + ) -> Self { + Self { + model, + db, + watching, + status, + } + } +} + +impl output_channel::Server for NotifyForwarder { + // Stores the message, sends a system notification, forwards the message to watching clients + fn send_message( + &mut self, + params: output_channel::SendMessageParams, + _results: output_channel::SendMessageResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let request = pry!(params.get()); + let message = pry!(request.get_message()); + + // Store in database + let already_stored: bool = { + // If this fails parsing, the message is not valid at all. + // The server is probably misbehaving. + let min_message: MinMessage = pry!(serde_json::from_str(&message) + .map_err(|e| Error::InvalidMinMessage(message.to_string(), e))); + let model = self.model.borrow(); + match self.db.insert_message(&model.server, message) { + Err(Error::DuplicateMessage) => { + warn!(min_message = ?min_message, "Received duplicate message"); + true + } + Err(e) => { + error!(min_message = ?min_message, error = ?e, "Can't store the message"); + false + } + _ => false, + } + }; + + if !already_stored { + // Show notification + // Our priority is to show notifications. If anything fails, panic. + if !{ self.model.borrow().muted } { + let msg: Message = pry!(serde_json::from_str(&message) + .map_err(|e| Error::InvalidMessage(message.to_string(), e))); + tokio::task::spawn_local(async move { + let proxy = match NotificationProxy::new().await { + Ok(p) => p, + Err(e) => { + panic!("Can't show notification: {:?}", e); + } + }; + + let title = msg.display_title(); + let title = title.as_ref().map(|x| x.as_str()).unwrap_or(&msg.topic); + + let n = Notification::new(&title).body( + msg.display_message() + .as_ref() + .map(|x| x.as_str()) + .unwrap_or(""), + ); + + let notification_id = "com.ranfdev.Notify"; + info!("Showing notification"); + proxy.add_notification(notification_id, n).await.unwrap(); + }); + } + + // Forward + if let Some(watching) = self.watching.upgrade() { + let watching = watching.borrow(); + let futs = watching.iter().map(|(_id, w)| { + let mut req = w.send_message_request(); + req.get().set_message(message); + async move { + if let Err(e) = req.send().promise.await { + error!(error = ?e, "Error forwarding"); + } + } + }); + tokio::task::spawn_local(join_all(futs)); + } + } + + Promise::from_future(async move { + // some backpressure + tokio::time::sleep(MESSAGE_THROTTLE).await; + Ok(()) + }) + } + + fn send_status( + &mut self, + params: output_channel::SendStatusParams, + _: output_channel::SendStatusResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let status = pry!(pry!(params.get()).get_status()); + if let Some(watching) = self.watching.upgrade() { + for (_, w) in watching.borrow().iter() { + let mut req = w.send_status_request(); + req.get().set_status(status); + tokio::task::spawn_local(async move { + req.send().promise.await.unwrap(); + }); + } + } + self.status.set(status); + Promise::ok(()) + } +} + +struct WatcherImpl { + id: generational_arena::Index, + watchers: Weak>>, +} + +impl watch_handle::Server for WatcherImpl {} + +impl Drop for WatcherImpl { + fn drop(&mut self) { + if let Some(w) = self.watchers.upgrade() { + w.borrow_mut().remove(self.id); + } + } +} + +pub struct SubscriptionImpl { + model: Rc>, + db: Db, + server: ntfy_proxy::Client, + server_watch_handle: OnceCell, + watchers: Rc>>, + status: Rc>, +} + +impl SubscriptionImpl { + fn new(model: models::Subscription, server: ntfy_proxy::Client, db: Db) -> Self { + Self { + model: Rc::new(RefCell::new(model)), + server, + db, + watchers: Default::default(), + server_watch_handle: Default::default(), + status: Rc::new(Cell::new(Status::Down)), + } + } + + fn output_channel(&self) -> NotifyForwarder { + NotifyForwarder::new( + self.model.clone(), + self.db.clone(), + Rc::downgrade(&self.watchers), + self.status.clone(), + ) + } +} + +impl subscription::Server for SubscriptionImpl { + fn watch( + &mut self, + params: subscription::WatchParams, + mut results: subscription::WatchResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let watcher = pry!(pry!(params.get()).get_watcher()); + let since = pry!(params.get()).get_since(); + + // Send old messages + let msgs = { + let model = self.model.borrow(); + pry!(self + .db + .list_messages(&model.server, &model.topic, since) + .map_err(Error::Db)) + }; + + let futs = msgs.into_iter().map(move |msg| { + let mut req = watcher.send_message_request(); + req.get().set_message(&msg); + req.send().promise + }); + + let watcher = pry!(pry!(params.get()).get_watcher()); + let mut req = watcher.send_status_request(); + req.get().set_status(self.status.get()); + + let id = { self.watchers.borrow_mut().insert(watcher) }; + + results.get().set_handle(capnp_rpc::new_client(WatcherImpl { + id, + watchers: Rc::downgrade(&self.watchers), + })); + + Promise::from_future(async move { + futures::future::try_join_all(futs).await?; + req.send().promise.await?; + Ok(()) + }) + } + fn publish( + &mut self, + params: subscription::PublishParams, + _results: subscription::PublishResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let msg = pry!(pry!(params.get()).get_message()); + + let mut req = self.server.publish_request(); + req.get().set_message(msg); + + Promise::from_future(async move { + req.send().promise.await?; + Ok(()) + }) + } + fn get_info( + &mut self, + _: subscription::GetInfoParams, + mut results: subscription::GetInfoResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let mut res = results.get(); + let model = self.model.borrow(); + res.set_server(&model.server); + res.set_display_name(&model.display_name); + res.set_topic(&model.topic); + res.set_muted(model.muted); + res.set_read_until(model.read_until); + Promise::ok(()) + } + fn update_info( + &mut self, + params: subscription::UpdateInfoParams, + _results: subscription::UpdateInfoResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let info = pry!(pry!(params.get()).get_value()); + let mut model = self.model.borrow_mut(); + model.display_name = pry!(info.get_display_name()).to_string(); + model.muted = info.get_muted(); + model.read_until = info.get_read_until(); + pry!(self.db.update_subscription(model.clone())); + Promise::ok(()) + } + fn clear_notifications( + &mut self, + _params: subscription::ClearNotificationsParams, + _results: subscription::ClearNotificationsResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let model = self.model.borrow_mut(); + pry!(self.db.delete_messages(&model.server, &model.topic)); + Promise::ok(()) + } + + fn update_read_until( + &mut self, + params: subscription::UpdateReadUntilParams, + _: subscription::UpdateReadUntilResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let value = pry!(params.get()).get_value(); + let mut model = self.model.borrow_mut(); + pry!(self + .db + .update_read_until(&model.server, &model.topic, value)); + model.read_until = value; + Promise::ok(()) + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct WatchKey { + server: String, + topic: String, +} +pub struct SystemNotifier { + servers: HashMap, + watching: Rc>>, + db: Db, +} + +impl SystemNotifier { + pub fn new(dbpath: &str) -> Self { + Self { + servers: HashMap::new(), + watching: Rc::new(RefCell::new(HashMap::new())), + db: Db::connect(dbpath).unwrap(), + } + } + fn watch(&mut self, sub: models::Subscription) -> Promise { + let ntfy = self + .servers + .entry(sub.server.to_owned()) + .or_insert_with(|| capnp_rpc::new_client(NtfyProxyImpl::new(sub.server.to_owned()))); + + let subscription = SubscriptionImpl::new(sub.clone(), ntfy.clone(), self.db.clone()); + + let mut req = ntfy.watch_request(); + req.get().set_topic(&sub.topic); + req.get() + .set_watcher(capnp_rpc::new_client(subscription.output_channel())); + let res = req.send(); + let handle = res.pipeline.get_handle(); + subscription + .server_watch_handle + .set(handle) + .map_err(|_| "already set") + .unwrap(); + + let watching = self.watching.clone(); + let subc: subscription::Client = capnp_rpc::new_client(subscription); + + Promise::from_future(async move { + res.promise + .await + .map_err(|e| capnp::Error::failed(e.to_string()))?; + watching.borrow_mut().insert( + WatchKey { + server: sub.server.to_owned(), + topic: sub.topic.to_owned(), + }, + subc.clone(), + ); + Ok(subc) + }) + } + pub fn watch_subscribed(&mut self) -> Promise<(), capnp::Error> { + let f: Vec<_> = pry!(self.db.list_subscriptions()) + .into_iter() + .map(|m| self.watch(m.clone())) + .collect(); + Promise::from_future(async move { + join_all(f.into_iter().map(|x| async move { + if let Err(e) = x.await { + error!(error = ?e, "Can't rewatch subscribed topic"); + } + })) + .await; + Ok(()) + }) + } +} + +impl system_notifier::Server for SystemNotifier { + fn subscribe( + &mut self, + params: system_notifier::SubscribeParams, + mut results: system_notifier::SubscribeResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let topic = pry!(pry!(params.get()).get_topic()); + let server: &str = pry!(pry!(params.get()).get_server()); + let server = if server.is_empty() { + "https://ntfy.sh" + } else { + "" + }; + + let subscription = pry!( + models::Subscription::builder(server.to_owned(), topic.to_owned()) + .build() + .map_err(|e| capnp::Error::failed(e.to_string())) + ); + let sub: Promise = self.watch(subscription.clone()); + + let mut db = self.db.clone(); + Promise::from_future(async move { + results.get().set_subscription(sub.await?); + + db.insert_subscription(subscription).map_err(|e| { + capnp::Error::failed(format!("could not insert subscription: {}", e)) + })?; + Ok(()) + }) + } + fn unsubscribe( + &mut self, + params: system_notifier::UnsubscribeParams, + _results: system_notifier::UnsubscribeResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let topic = pry!(pry!(params.get()).get_topic()); + let server = pry!(pry!(params.get()).get_server()); + { + self.watching.borrow_mut().remove(&WatchKey { + server: server.to_string(), + topic: topic.to_string(), + }); + pry!(self + .db + .remove_subscription(&server, &topic) + .map_err(|e| capnp::Error::failed(e.to_string()))); + info!(server, topic, "Unsubscribed"); + } + Promise::ok(()) + } + fn list_subscriptions( + &mut self, + _: system_notifier::ListSubscriptionsParams, + mut results: system_notifier::ListSubscriptionsResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + let req = results.get(); + let values = self.watching.borrow().values().cloned().collect::>(); + let mut list = req.init_list(values.len() as u32); + + for (i, v) in values.iter().enumerate() { + use capnp::capability::FromClientHook; + list.set(i as u32, v.clone().clone().into_client_hook()); + } + + Promise::ok(()) + } +} + +pub fn start(socket_path: std::path::PathBuf, dbpath: &str) -> anyhow::Result<()> { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let listener = rt.block_on(async move { + let _ = std::fs::remove_file(&socket_path); + UnixListener::bind(&socket_path).unwrap() + }); + + let dbpath = dbpath.to_owned(); + let f = move || { + let local = tokio::task::LocalSet::new(); + let mut system_notifier = SystemNotifier::new(&dbpath); + local.spawn_local(async move { + system_notifier.watch_subscribed().await.unwrap(); + let system_client: system_notifier::Client = capnp_rpc::new_client(system_notifier); + + loop { + match listener.accept().await { + Ok((stream, _addr)) => { + info!("client connected"); + let (reader, writer) = + tokio_util::compat::TokioAsyncReadCompatExt::compat(stream).split(); + let network = twoparty::VatNetwork::new( + reader, + writer, + rpc_twoparty_capnp::Side::Server, + Default::default(), + ); + + let rpc_system = + RpcSystem::new(Box::new(network), Some(system_client.clone().client)); + + tokio::task::spawn_local(rpc_system); + } + Err(e) => { + error!(error=%e); + } + } + } + }); + rt.block_on(local); + }; + std::thread::spawn(move || { + f(); + }); + + Ok(()) +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..6510695 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +unstable_features = true +group_imports = "StdExternalCrate" diff --git a/src/application.rs b/src/application.rs index 335e246..1e23dd9 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,54 +1,51 @@ -use gettextrs::gettext; -use tracing::{debug, info}; - use adw::subclass::prelude::*; +use capnp_rpc::{rpc_twoparty_capnp, twoparty, RpcSystem}; +use futures::{AsyncRead, AsyncReadExt, AsyncWrite}; +use gettextrs::gettext; +use gio::SocketClient; +use gio::UnixSocketAddress; use gtk::prelude::*; use gtk::{gdk, gio, glib}; +use ntfy_daemon::ntfy_capnp::system_notifier; +use tracing::{debug, info}; use crate::config::{APP_ID, PKGDATADIR, PROFILE, VERSION}; -use crate::window::ExampleApplicationWindow; +use crate::widgets::*; + +trait RW: AsyncRead + AsyncWrite {} +impl RW for T {} mod imp { - use super::*; + use std::cell::RefCell; + use glib::WeakRef; use once_cell::sync::OnceCell; - #[derive(Debug, Default)] - pub struct ExampleApplication { - pub window: OnceCell>, + use super::*; + + #[derive(Default)] + pub struct NotifyApplication { + pub window: RefCell>, + pub hold_guard: OnceCell, } #[glib::object_subclass] - impl ObjectSubclass for ExampleApplication { - const NAME: &'static str = "ExampleApplication"; - type Type = super::ExampleApplication; + impl ObjectSubclass for NotifyApplication { + const NAME: &'static str = "NotifyApplication"; + type Type = super::NotifyApplication; type ParentType = adw::Application; } - impl ObjectImpl for ExampleApplication {} + impl ObjectImpl for NotifyApplication {} - impl ApplicationImpl for ExampleApplication { + impl ApplicationImpl for NotifyApplication { fn activate(&self) { - debug!("AdwApplication::activate"); + debug!("AdwApplication::activate"); self.parent_activate(); - let app = self.obj(); - - if let Some(window) = self.window.get() { - let window = window.upgrade().unwrap(); - window.present(); - return; - } - - let window = ExampleApplicationWindow::new(&app); - self.window - .set(window.downgrade()) - .expect("Window already set."); - - app.main_window().present(); } fn startup(&self) { - debug!("AdwApplication::startup"); + debug!("AdwApplication::startup"); self.parent_startup(); let app = self.obj(); @@ -59,21 +56,56 @@ mod imp { app.setup_gactions(); app.setup_accels(); } + fn command_line(&self, command_line: &gio::ApplicationCommandLine) -> glib::ExitCode { + let socket_path = glib::user_data_dir().join("com.ranfdev.Notify.socket"); + + debug!("AdwApplication::command_line"); + let arguments = command_line.arguments(); + let is_daemon = arguments.get(1).map(|x| x.to_str()) == Some(Some("--daemon")); + let app = self.obj(); + + if self.hold_guard.get().is_none() { + self.obj().ensure_rpc_running(&socket_path); + } + + glib::MainContext::default().spawn_local(async move { + super::NotifyApplication::run_in_background().await.unwrap(); + }); + + if is_daemon { + return glib::ExitCode::SUCCESS; + } + + { + let w = self.window.borrow(); + if let Some(window) = w.upgrade() { + if window.is_visible() { + window.present(); + return glib::ExitCode::SUCCESS; + } + } + } + + app.build_window(&socket_path); + app.main_window().present(); + + glib::ExitCode::SUCCESS + } } - impl GtkApplicationImpl for ExampleApplication {} - impl AdwApplicationImpl for ExampleApplication {} + impl GtkApplicationImpl for NotifyApplication {} + impl AdwApplicationImpl for NotifyApplication {} } glib::wrapper! { - pub struct ExampleApplication(ObjectSubclass) + pub struct NotifyApplication(ObjectSubclass) @extends gio::Application, gtk::Application, @implements gio::ActionMap, gio::ActionGroup; } -impl ExampleApplication { - fn main_window(&self) -> ExampleApplicationWindow { - self.imp().window.get().unwrap().upgrade().unwrap() +impl NotifyApplication { + fn main_window(&self) -> NotifyWindow { + self.imp().window.borrow().upgrade().unwrap() } fn setup_gactions(&self) { @@ -105,7 +137,7 @@ impl ExampleApplication { let provider = gtk::CssProvider::new(); provider.load_from_resource("/com/ranfdev/Notify/style.css"); if let Some(display) = gdk::Display::default() { - gtk::StyleContext::add_provider_for_display( + gtk::style_context_add_provider_for_display( &display, &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, @@ -117,10 +149,7 @@ impl ExampleApplication { let dialog = adw::AboutWindow::builder() .application_icon(APP_ID) .application_name("Notify") - // Insert your license of choice here - // .license_type(gtk::License::MitX11) - // Insert your website here - // .website("https://gitlab.gnome.org/bilelmoussaoui/notify/") + .license_type(gtk::License::Gpl30) .version(VERSION) .transient_for(&self.main_window()) .translator_credits(gettext("translator-credits")) @@ -133,18 +162,67 @@ impl ExampleApplication { } pub fn run(&self) -> glib::ExitCode { - info!("Notify ({})", APP_ID); - info!("Version: {} ({})", VERSION, PROFILE); - info!("Datadir: {}", PKGDATADIR); + info!(app_id = %APP_ID, version = %VERSION, profile = %PROFILE, datadir = %PKGDATADIR, "running"); ApplicationExtManual::run(self) } + async fn run_in_background() -> ashpd::Result<()> { + let response = ashpd::desktop::background::Background::request() + .reason("Listen for coming notifications") + .auto_start(true) + .command(&["notify", "--daemon"]) + .dbus_activatable(false) + .send() + .await? + .response()?; + + info!(auto_start = %response.auto_start(), run_in_background = %response.run_in_background()); + + Ok(()) + } + + fn ensure_rpc_running(&self, socket_path: &Path) { + let dbpath = glib::user_data_dir().join("com.ranfdev.Notify.sqlite"); + info!(database_path = %dbpath.display()); + ntfy_daemon::system_client::start(socket.to_owned(), dbpath.to_str().unwrap()).unwrap(); + self.imp().hold_guard.set(self.hold()).unwrap(); + } + + fn build_window(&self, socket_path: &Path) { + let address = UnixSocketAddress::new(socket_path); + let client = SocketClient::new(); + let connection = + SocketClientExt::connect(&client, &address, gio::Cancellable::NONE).unwrap(); + + let rw = connection.into_async_read_write().unwrap(); + let (reader, writer) = rw.split(); + + let rpc_network = Box::new(twoparty::VatNetwork::new( + reader, + writer, + rpc_twoparty_capnp::Side::Client, + Default::default(), + )); + let mut rpc_system = RpcSystem::new(rpc_network, None); + let client: system_notifier::Client = + rpc_system.bootstrap(rpc_twoparty_capnp::Side::Server); + + glib::MainContext::default().spawn_local(async move { + debug!("rpc_system started"); + rpc_system.await.unwrap(); + debug!("rpc_system stopped"); + }); + + let window = NotifyWindow::new(self, client); + *self.imp().window.borrow_mut() = window.downgrade(); + } } -impl Default for ExampleApplication { +impl Default for NotifyApplication { fn default() -> Self { glib::Object::builder() .property("application-id", APP_ID) + .property("flags", gio::ApplicationFlags::HANDLES_COMMAND_LINE) .property("resource-base-path", "/com/ranfdev/Notify/") .build() } diff --git a/src/async_utils.rs b/src/async_utils.rs new file mode 100644 index 0000000..6c50d3b --- /dev/null +++ b/src/async_utils.rs @@ -0,0 +1,28 @@ +use std::cell::Cell; +use std::rc::Rc; + +use glib::Receiver; +use glib::SourceId; +use gtk::glib; + +pub fn debounce_channel( + duration: std::time::Duration, + source: Receiver, +) -> Receiver { + let (tx, rx) = glib::MainContext::channel(Default::default()); + let scheduled = Rc::new(Cell::new(None::)); + source.attach(None, move |data| { + if let Some(scheduled) = scheduled.take() { + scheduled.remove(); + } + let tx = tx.clone(); + let scheduled_clone = scheduled.clone(); + let source_id = glib::source::timeout_add_local_once(duration, move || { + tx.send(data).unwrap(); + scheduled_clone.take(); + }); + scheduled.set(Some(source_id)); + glib::ControlFlow::Continue + }); + rx +} diff --git a/src/main.rs b/src/main.rs index 4b10cfa..b7bf074 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ mod application; #[rustfmt::skip] mod config; -mod window; +mod async_utils; +mod subscription; +pub mod widgets; use gettextrs::{gettext, LocaleCategory}; use gtk::{gio, glib}; -use self::application::ExampleApplication; +use self::application::NotifyApplication; use self::config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; fn main() -> glib::ExitCode { @@ -23,6 +25,6 @@ fn main() -> glib::ExitCode { let res = gio::Resource::load(RESOURCES_FILE).expect("Could not load gresource file"); gio::resources_register(&res); - let app = ExampleApplication::default(); + let app = NotifyApplication::default(); app.run() } diff --git a/src/subscription.rs b/src/subscription.rs new file mode 100644 index 0000000..dd04c5b --- /dev/null +++ b/src/subscription.rs @@ -0,0 +1,318 @@ +use std::cell::{Cell, OnceCell, RefCell}; +use std::rc::Rc; + +use adw::prelude::*; +use capnp::capability::Promise; +use capnp_rpc::pry; +use glib::once_cell::sync::Lazy; +use glib::subclass::prelude::*; +use glib::subclass::Signal; +use glib::Properties; +use gtk::{gio, glib}; +use ntfy_daemon::models; +use ntfy_daemon::ntfy_capnp::{output_channel, subscription, watch_handle, Status}; +use tracing::{debug, debug_span, error, instrument}; + +struct TopicWatcher { + sub: glib::WeakRef, +} +impl output_channel::Server for TopicWatcher { + fn send_message( + &mut self, + params: output_channel::SendMessageParams, + _results: output_channel::SendMessageResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + if let Some(sub) = self.sub.upgrade() { + let request = pry!(params.get()); + let message = pry!(request.get_message()); + + let msg: models::Message = serde_json::from_str(&message).unwrap(); + sub.imp().messages.append(&glib::BoxedAnyObject::new(msg)); + sub.update_unread_count(); + Promise::ok(()) + } else { + Promise::err(capnp::Error::failed("dead channel".to_string())) + } + } + fn send_status( + &mut self, + params: output_channel::SendStatusParams, + _: output_channel::SendStatusResults, + ) -> capnp::capability::Promise<(), capnp::Error> { + if let Some(sub) = self.sub.upgrade() { + let status = pry!(pry!(params.get()).get_status()); + sub.imp().status.set(status); + sub.notify_status(); + Promise::ok(()) + } else { + Promise::err(capnp::Error::failed("dead channel".to_string())) + } + } +} + +impl Drop for TopicWatcher { + fn drop(&mut self) { + debug!("Dropped topic watcher"); + } +} + +mod imp { + use super::*; + + #[derive(Properties)] + #[properties(wrapper_type = super::Subscription)] + pub struct Subscription { + #[property(get)] + pub display_name: RefCell, + #[property(get)] + pub topic: RefCell, + #[property(get)] + pub url: RefCell, + #[property(get)] + pub server: RefCell, + #[property(get = Self::get_status, type = u8)] + pub status: Rc>, + #[property(get)] + pub muted: Cell, + #[property(get)] + pub unread_count: Cell, + pub read_until: Cell, + pub messages: gio::ListStore, + pub client: OnceCell, + pub remote_handle: RefCell>, + } + + impl Subscription { + fn get_status(&self) -> u8 { + let s: u16 = Cell::get(&self.status).into(); + s as u8 + } + } + + impl Default for Subscription { + fn default() -> Self { + Self { + display_name: Default::default(), + topic: Default::default(), + url: Default::default(), + muted: Default::default(), + server: Default::default(), + status: Rc::new(Cell::new(Status::Down)), + messages: gio::ListStore::new::(), + client: Default::default(), + unread_count: Default::default(), + read_until: Default::default(), + remote_handle: Default::default(), + } + } + } + + #[glib::derived_properties] + impl ObjectImpl for Subscription { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("awarded").build()]); + SIGNALS.as_ref() + } + } + + #[glib::object_subclass] + impl ObjectSubclass for Subscription { + const NAME: &'static str = "TopicSubscription"; + type Type = super::Subscription; + } +} + +glib::wrapper! { + pub struct Subscription(ObjectSubclass); +} + +impl Subscription { + pub fn new(client: subscription::Client) -> Self { + let this: Self = glib::Object::builder().build(); + let imp = this.imp(); + if let Err(_) = imp.client.set(client) { + panic!(); + }; + + let this_clone = this.clone(); + glib::MainContext::default().spawn_local(async move { + match this_clone.load().await { + Ok(_) => {} + Err(e) => { + error!(error = %e, "loading subscription data"); + } + } + }); + this + } + + fn init_info( + &self, + topic: &str, + server: &str, + muted: bool, + read_until: u64, + display_name: &str, + ) { + let imp = self.imp(); + imp.topic.replace(topic.to_string()); + self.notify_topic(); + imp.server.replace(server.to_string()); + self.notify_server(); + imp.muted.replace(muted); + self.notify_muted(); + imp.read_until.replace(read_until); + self.notify_unread_count(); + self._set_display_name(display_name.to_string()); + } + + fn load(&self) -> Promise<(), capnp::Error> { + let imp = self.imp(); + let req_info = imp.client.get().unwrap().get_info_request(); + let req_messages = { + let mut req = imp.client.get().unwrap().watch_request(); + req.get().set_watcher(capnp_rpc::new_client(TopicWatcher { + sub: self.downgrade(), + })); + req + }; + + let this = self.clone(); + Promise::from_future(async move { + let info = req_info.send().promise.await?; + let info = info.get()?; + this.init_info( + info.get_topic()?, + info.get_server()?, + info.get_muted(), + info.get_read_until(), + info.get_display_name()?, + ); + + let message_stream = req_messages.send().promise.await?; + let handle = message_stream.get()?.get_handle()?; + this.imp().remote_handle.replace(Some(handle)); + Ok(()) + }) + } + + fn _set_display_name(&self, value: String) { + let imp = self.imp(); + let value = if value.is_empty() { + self.topic() + } else { + value + }; + imp.display_name.replace(value); + self.notify_display_name(); + } + #[instrument(skip_all)] + pub fn set_display_name(&self, value: String) -> Promise<(), capnp::Error> { + let this = self.clone(); + Promise::from_future(async move { + this._set_display_name(value); + this.send_updated_info().await?; + Ok(()) + }) + } + + fn send_updated_info(&self) -> Promise<(), capnp::Error> { + let imp = self.imp(); + let mut req = imp.client.get().unwrap().update_info_request(); + let mut val = pry!(req.get().get_value()); + val.set_muted(imp.muted.get()); + val.set_display_name(&*imp.display_name.borrow()); + val.set_read_until(imp.read_until.get()); + Promise::from_future(async move { + let _span = debug_span!("send_updated_info").entered(); + debug!("sending"); + req.send().promise.await?; + Ok(()) + }) + } + fn last_message(list: &gio::ListStore) -> Option { + let n = list.n_items(); + let last = list + .item(n.checked_sub(1)?) + .and_downcast::()?; + let last = last.borrow::(); + Some(last.clone()) + } + fn update_unread_count(&self) { + let imp = self.imp(); + if let Some(last) = Self::last_message(&imp.messages) { + if last.time > imp.read_until.get() { + imp.unread_count.set(1); + } else { + imp.unread_count.set(0); + } + } else { + imp.unread_count.set(0); + } + self.notify_unread_count(); + } + + pub fn set_muted(&self, value: bool) -> Promise<(), capnp::Error> { + let this = self.clone(); + Promise::from_future(async move { + this.imp().muted.replace(value); + this.notify_muted(); + this.send_updated_info().await?; + Ok(()) + }) + } + pub fn flag_all_as_read(&self) -> Promise<(), capnp::Error> { + let imp = self.imp(); + let Some(last) = Self::last_message(&imp.messages) else { + return Promise::ok(()); + }; + let value = last.time; + + let this = self.clone(); + Promise::from_future(async move { + let mut req = this.imp().client.get().unwrap().update_read_until_request(); + req.get().set_value(value); + req.send().promise.await?; + this.imp().read_until.set(value); + this.update_unread_count(); + Ok(()) + }) + } + pub fn publish(&self, message: &str) -> Promise<(), capnp::Error> { + let imp = self.imp(); + let mut req = imp.client.get().unwrap().publish_request(); + let msg = serde_json::to_string(&models::Message { + topic: self.topic(), + message: Some(message.to_string()), + ..models::Message::default() + }) + .map_err(|e| capnp::Error::failed(e.to_string())); + + req.get().set_message(&pry!(msg)); + + Promise::from_future(async move { + let _span = debug_span!("publish").entered(); + debug!("sending"); + req.send().promise.await?; + Ok(()) + }) + } + #[instrument(skip_all)] + pub fn clear_notifications(&self) -> Promise<(), capnp::Error> { + let imp = self.imp(); + let req = imp.client.get().unwrap().clear_notifications_request(); + let this = self.clone(); + Promise::from_future(async move { + let _span = debug_span!("clear_notifications").entered(); + debug!("sending"); + req.send().promise.await?; + this.imp().messages.remove_all(); + Ok(()) + }) + } + + pub fn nice_status(&self) -> Status { + Status::try_from(self.imp().status.get() as u16).unwrap() + } +} diff --git a/src/widgets/add_subscription_dialog.rs b/src/widgets/add_subscription_dialog.rs new file mode 100644 index 0000000..2123c9d --- /dev/null +++ b/src/widgets/add_subscription_dialog.rs @@ -0,0 +1,180 @@ +use adw::prelude::*; +use adw::subclass::prelude::*; +use glib::once_cell::sync::Lazy; +use glib::subclass::Signal; +use glib::Properties; +use gtk::gio; +use gtk::glib; + +mod imp { + pub use super::*; + #[derive(Debug, Default, Properties)] + #[properties(wrapper_type = super::AddSubscriptionDialog)] + pub struct AddSubscriptionDialog { + #[property(name = "topic", get = |imp: &Self| imp.topic_entry.text(), type = glib::GString)] + pub topic_entry: adw::EntryRow, + #[property(name = "server", get = |imp: &Self| imp.server_entry.text(), type = glib::GString)] + pub server_entry: adw::EntryRow, + } + + #[glib::object_subclass] + impl ObjectSubclass for AddSubscriptionDialog { + const NAME: &'static str = "AddSubscriptionDialog"; + type Type = super::AddSubscriptionDialog; + type ParentType = adw::Window; + + fn class_init(klass: &mut Self::Class) { + klass.add_binding_action( + gtk::gdk::Key::Escape, + gtk::gdk::ModifierType::empty(), + "window.close", + None, + ); + klass.install_action("default.activate", None, |this, _, _| { + this.emit_subscribe_request(); + this.close(); + }); + } + } + #[glib::derived_properties] + impl ObjectImpl for AddSubscriptionDialog { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("subscribe-request").build()]); + SIGNALS.as_ref() + } + + fn constructed(&self) { + self.parent_constructed(); + let obj = self.obj().clone(); + obj.build_ui(); + } + } + impl WidgetImpl for AddSubscriptionDialog {} + impl WindowImpl for AddSubscriptionDialog {} + impl AdwWindowImpl for AddSubscriptionDialog {} +} + +glib::wrapper! { + pub struct AddSubscriptionDialog(ObjectSubclass) + @extends gtk::Widget, gtk::Window, adw::Window, + @implements gio::ActionMap, gio::ActionGroup, gtk::Root; +} + +impl AddSubscriptionDialog { + pub fn new() -> Self { + glib::Object::builder().build() + } + fn build_ui(&self) { + let imp = self.imp(); + let obj = self.clone(); + obj.set_title(Some("Subscribe To Topic")); + obj.set_modal(true); + obj.set_default_width(360); + + let toolbar_view = adw::ToolbarView::new(); + toolbar_view.add_top_bar(&adw::HeaderBar::new()); + + let content = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(12) + .margin_end(12) + .margin_start(12) + .margin_top(12) + .margin_bottom(12) + .build(); + let clamp = adw::Clamp::new(); + clamp.set_child(Some(&content)); + + let description = { + let d = gtk::Label::builder() + .label("Topics may not be password-protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications.") + .wrap(true) + .xalign(0.0) + .wrap_mode(gtk::pango::WrapMode::WordChar) + .build(); + d.add_css_class("dim-label"); + d + }; + + content.append(&description); + + let topic_entry = { + let e = &imp.topic_entry; + e.set_title("Topic"); + e.set_activates_default(true); + + let rand_btn = { + let b = gtk::Button::builder() + .icon_name("dice3-symbolic") + .tooltip_text("Generate Name") + .valign(gtk::Align::Center) + .css_classes(["flat"]) + .build(); + let ec = e.clone(); + b.connect_clicked(move |_| { + use rand::distributions::Alphanumeric; + use rand::{thread_rng, Rng}; + let mut rng = thread_rng(); + let chars: String = (0..10).map(|_| rng.sample(Alphanumeric) as char).collect(); + ec.set_text(&chars); + }); + b + }; + + e.add_suffix(&rand_btn); + e + }; + // TODO: Reserved topics + /*let reserved_switch = { + adw::SwitchRow::builder() + .title("Reserved") + .subtitle("For Ntfy Pro users only") + .build() + };*/ + let server_entry = &imp.server_entry; + server_entry.set_title("Server"); + + let expander_row = { + let e = adw::ExpanderRow::builder() + .title("Custom Server...") + .enable_expansion(false) + .show_enable_switch(true) + .build(); + e.add_row(server_entry); + e + }; + let list_box = { + let l = gtk::ListBox::new(); + l.add_css_class("boxed-list"); + l.append(topic_entry); + // l.append(&reserved_switch); + l.append(&expander_row); + l + }; + content.append(&list_box); + + let sub_btn = { + let b = gtk::Button::new(); + b.set_label("Subscribe"); + b.add_css_class("suggested-action"); + b.add_css_class("pill"); + b.set_halign(gtk::Align::Center); + + let wc = obj.clone(); + b.connect_clicked(move |_| { + wc.emit_subscribe_request(); + wc.close(); + }); + b + }; + + content.append(&sub_btn); + toolbar_view.set_content(Some(&clamp)); + + obj.set_content(Some(&toolbar_view)); + } + fn emit_subscribe_request(&self) { + self.emit_by_name::<()>("subscribe-request", &[]); + } +} diff --git a/src/widgets/message_row.rs b/src/widgets/message_row.rs new file mode 100644 index 0000000..d81e312 --- /dev/null +++ b/src/widgets/message_row.rs @@ -0,0 +1,209 @@ +use adw::prelude::*; +use adw::subclass::prelude::*; +use chrono::NaiveDateTime; +use gtk::{gio, glib}; +use ntfy_daemon::models; +use tracing::error; + +use crate::widgets::*; + +mod imp { + use super::*; + + #[derive(Debug, Default)] + pub struct MessageRow {} + + #[glib::object_subclass] + impl ObjectSubclass for MessageRow { + const NAME: &'static str = "MessageRow"; + type Type = super::MessageRow; + type ParentType = adw::Bin; + } + + impl ObjectImpl for MessageRow {} + + impl WidgetImpl for MessageRow {} + impl BinImpl for MessageRow {} +} + +glib::wrapper! { + pub struct MessageRow(ObjectSubclass) + @extends gtk::Widget, adw::Bin; +} + +impl MessageRow { + pub fn new(msg: models::Message) -> Self { + let this: Self = glib::Object::new(); + this.build_ui(msg); + this + } + fn build_ui(&self, msg: models::Message) { + let top_box = gtk::Box::new(gtk::Orientation::Horizontal, 8); + + let time = gtk::Label::builder() + .label( + &NaiveDateTime::from_timestamp_opt(msg.time as i64, 0) + .map(|time| time.format("%Y-%m-%d %H:%M:%S").to_string()) + .unwrap_or_default(), + ) + .wrap_mode(gtk::pango::WrapMode::WordChar) + .xalign(0.0) + .wrap(true) + .build(); + time.add_css_class("caption"); + + top_box.append(&time); + + if let Some(p) = msg.priority { + let text = format!( + "Priority: {}", + match p { + 5 => "Max", + 4 => "High", + 3 => "Medium", + 2 => "Low", + 1 => "Min", + _ => "Invalid", + } + ); + let priority = gtk::Label::builder() + .label(&text) + .wrap_mode(gtk::pango::WrapMode::WordChar) + .xalign(0.0) + .wrap(true) + .build(); + priority.add_css_class("caption"); + priority.add_css_class("chip"); + if p == 5 { + priority.add_css_class("chip--danger") + } else if p == 4 { + priority.add_css_class("chip--warning") + } + top_box.append(&priority); + } + + let b = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .margin_top(8) + .margin_bottom(8) + .margin_start(8) + .margin_end(8) + .build(); + + b.append(&top_box); + if let Some(title) = msg.display_title() { + let label = gtk::Label::builder() + .label(&title) + .wrap_mode(gtk::pango::WrapMode::WordChar) + .xalign(0.0) + .wrap(true) + .selectable(true) + .build(); + label.add_css_class("heading"); + b.append(&label); + } + + if let Some(message) = msg.display_message() { + let label = gtk::Label::builder() + .label(&message) + .wrap_mode(gtk::pango::WrapMode::WordChar) + .xalign(0.0) + .wrap(true) + .selectable(true) + .build(); + b.append(&label); + } + + if msg.actions.len() > 0 { + let action_btns = gtk::Box::builder().spacing(8).build(); + + for a in msg.actions { + let btn = self.build_action_btn(a); + action_btns.append(&btn); + } + + b.append(&action_btns); + } + if msg.tags.len() > 0 { + let mut tags_text = String::from("tags: "); + tags_text.push_str(&msg.tags.join(", ")); + let tags = gtk::Label::builder() + .label(&tags_text) + .xalign(0.0) + .wrap(true) + .wrap_mode(gtk::pango::WrapMode::WordChar) + .build(); + b.append(&tags); + } + + self.set_child(Some(&b)); + } + fn build_action_btn(&self, action: models::Action) -> gtk::Button { + let btn = gtk::Button::new(); + match action { + models::Action::View { label, url, .. } => { + btn.set_label(&label); + btn.set_tooltip_text(Some(&format!("Go to {url}"))); + btn.connect_clicked(move |_| { + gtk::UriLauncher::builder().uri(url.clone()).build().launch( + gtk::Window::NONE, + gio::Cancellable::NONE, + |_| {}, + ); + }); + } + models::Action::Http { + label, + method, + url, + body, + headers, + .. + } => { + btn.set_label(&label); + btn.set_tooltip_text(Some(&format!("Send HTTP {method} to {url}"))); + let (tx, rx) = glib::MainContext::channel(Default::default()); + let this = self.clone(); + btn.connect_clicked({ + let url = url.clone(); + let method = method.clone(); + move |_| { + let url = url.clone(); + let method = method.clone(); + let tx = tx.clone(); + let body = body.clone(); + let headers = headers.clone(); + gio::spawn_blocking(move || { + let mut req = ureq::request(method.as_str(), url.as_str()); + for (k, v) in headers.iter() { + req = req.set(&k, &v); + } + tx.send(req.send(body.as_bytes())).unwrap(); + }); + } + }); + rx.attach(Some(&glib::MainContext::default()), move |res| { + let method = method.clone(); + let url = url.clone(); + this.spawn_with_near_toast(async move { + match res { + Err(e) => { + error!(error = ?e, "Error sending request"); + Err(format!("Error sending HTTP {method} to {url}")) + } + Ok(_) => Ok(()), + } + }); + glib::ControlFlow::Continue + }); + } + models::Action::Broadcast { label, .. } => { + btn.set_label(&label); + btn.set_sensitive(false); + btn.set_tooltip_text(Some("Broadcast action only available on Android")); + } + } + btn + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 0000000..e243c2b --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1,8 @@ +mod add_subscription_dialog; +mod message_row; +mod subscription_info_dialog; +mod window; +pub use add_subscription_dialog::AddSubscriptionDialog; +pub use message_row::*; +pub use subscription_info_dialog::SubscriptionInfoDialog; +pub use window::*; diff --git a/src/widgets/subscription_info_dialog.rs b/src/widgets/subscription_info_dialog.rs new file mode 100644 index 0000000..19eed2f --- /dev/null +++ b/src/widgets/subscription_info_dialog.rs @@ -0,0 +1,112 @@ +use std::cell::RefCell; + +use adw::prelude::*; +use adw::subclass::prelude::*; +use glib::Properties; +use gtk::gio; +use gtk::glib; + +use crate::widgets::*; + +mod imp { + pub use super::*; + #[derive(Debug, Default, Properties, gtk::CompositeTemplate)] + #[template(resource = "/com/ranfdev/Notify/ui/subscription_info_dialog.ui")] + #[properties(wrapper_type = super::SubscriptionInfoDialog)] + pub struct SubscriptionInfoDialog { + #[property(get, construct_only)] + pub subscription: RefCell>, + #[template_child] + pub display_name_entry: TemplateChild, + #[template_child] + pub muted_switch_row: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SubscriptionInfoDialog { + const NAME: &'static str = "SubscriptionInfoDialog"; + type Type = super::SubscriptionInfoDialog; + type ParentType = adw::Window; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.add_binding_action( + gtk::gdk::Key::Escape, + gtk::gdk::ModifierType::empty(), + "window.close", + None, + ); + } + + // You must call `Widget`'s `init_template()` within `instance_init()`. + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + #[glib::derived_properties] + impl ObjectImpl for SubscriptionInfoDialog { + fn constructed(&self) { + self.parent_constructed(); + let this = self.obj().clone(); + + let (tx, rx) = glib::MainContext::channel(glib::Priority::default()); + let rx = + crate::async_utils::debounce_channel(std::time::Duration::from_millis(500), rx); + rx.attach(None, move |entry| { + this.update_display_name(&entry); + glib::ControlFlow::Continue + }); + + let this = self.obj().clone(); + self.display_name_entry + .set_text(&this.subscription().unwrap().display_name()); + self.muted_switch_row + .set_active(this.subscription().unwrap().muted()); + + self.display_name_entry.connect_changed({ + move |entry| { + tx.send(entry.clone()).unwrap(); + } + }); + let this = self.obj().clone(); + self.muted_switch_row.connect_active_notify({ + move |switch| { + this.update_muted(switch); + } + }); + } + } + impl WidgetImpl for SubscriptionInfoDialog {} + impl WindowImpl for SubscriptionInfoDialog {} + impl AdwWindowImpl for SubscriptionInfoDialog {} +} + +glib::wrapper! { + pub struct SubscriptionInfoDialog(ObjectSubclass) + @extends gtk::Widget, gtk::Window, adw::Window, + @implements gio::ActionMap, gio::ActionGroup, gtk::Root; +} + +impl SubscriptionInfoDialog { + pub fn new(subscription: crate::subscription::Subscription) -> Self { + let this = glib::Object::builder() + .property("subscription", subscription) + .build(); + this + } + fn update_display_name(&self, entry: &impl IsA) { + if let Some(sub) = self.subscription() { + let entry = entry.clone(); + self.spawn_with_near_toast(async move { + let res = sub.set_display_name(entry.text().to_string()).await; + res + }); + } + } + fn update_muted(&self, switch: &adw::SwitchRow) { + if let Some(sub) = self.subscription() { + let switch = switch.clone(); + self.spawn_with_near_toast(async move { sub.set_muted(switch.is_active()).await }) + } + } +} diff --git a/src/widgets/window.rs b/src/widgets/window.rs new file mode 100644 index 0000000..8fbdd64 --- /dev/null +++ b/src/widgets/window.rs @@ -0,0 +1,483 @@ +use std::cell::Cell; +use std::cell::OnceCell; + +use adw::prelude::*; +use adw::subclass::prelude::*; +use futures::prelude::*; +use gtk::{gio, glib}; +use ntfy_daemon::models; +use ntfy_daemon::ntfy_capnp::{system_notifier, Status}; +use tracing::warn; + +use crate::application::NotifyApplication; +use crate::config::{APP_ID, PROFILE}; +use crate::subscription::Subscription; +use crate::widgets::*; + +pub trait SpawnWithToast { + fn spawn_with_near_toast( + &self, + f: impl Future> + 'static, + ); +} + +impl> SpawnWithToast for W { + fn spawn_with_near_toast( + &self, + f: impl Future> + 'static, + ) { + let p: Option = self.ancestor(NotifyWindow::static_type()).and_downcast(); + glib::MainContext::default().spawn_local(async move { + if let Err(e) = f.await { + if let Some(p) = p { + p.imp() + .toast_overlay + .add_toast(adw::Toast::builder().title(&e.to_string()).build()) + } + } + }); + } +} + +mod imp { + use super::*; + + #[derive(gtk::CompositeTemplate)] + #[template(resource = "/com/ranfdev/Notify/ui/window.ui")] + pub struct NotifyWindow { + #[template_child] + pub headerbar: TemplateChild, + #[template_child] + pub message_list: TemplateChild, + #[template_child] + pub subscription_list: TemplateChild, + #[template_child] + pub entry: TemplateChild, + #[template_child] + pub navigation_split_view: TemplateChild, + #[template_child] + pub subscription_view: TemplateChild, + #[template_child] + pub subscription_menu_btn: TemplateChild, + pub subscription_list_model: gio::ListStore, + #[template_child] + pub toast_overlay: TemplateChild, + #[template_child] + pub stack: TemplateChild, + #[template_child] + pub welcome_view: TemplateChild, + #[template_child] + pub list_view: TemplateChild, + #[template_child] + pub message_scroll: TemplateChild, + #[template_child] + pub banner: TemplateChild, + pub notifier: OnceCell, + pub conn: OnceCell, + pub settings: gio::Settings, + pub banner_binding: Cell>, + } + + impl Default for NotifyWindow { + fn default() -> Self { + let this = Self { + headerbar: TemplateChild::default(), + message_list: TemplateChild::default(), + entry: TemplateChild::default(), + subscription_view: TemplateChild::default(), + navigation_split_view: TemplateChild::default(), + subscription_menu_btn: TemplateChild::default(), + subscription_list: TemplateChild::default(), + toast_overlay: TemplateChild::default(), + stack: TemplateChild::default(), + welcome_view: TemplateChild::default(), + list_view: TemplateChild::default(), + message_scroll: TemplateChild::default(), + banner: TemplateChild::default(), + subscription_list_model: gio::ListStore::new::(), + settings: gio::Settings::new(APP_ID), + notifier: OnceCell::new(), + conn: OnceCell::new(), + banner_binding: Cell::new(None), + }; + + this + } + } + + #[gtk::template_callbacks] + impl NotifyWindow { + #[template_callback] + fn show_add_topic(&self, _btn: >k::Button) { + let dialog = AddSubscriptionDialog::new(); + dialog.set_transient_for(Some(&self.obj().clone())); + dialog.present(); + + let this = self.obj().clone(); + let dc = dialog.clone(); + dialog.connect_local("subscribe-request", true, move |_| { + this.add_subscription(&dc.server(), &dc.topic()); + None + }); + } + } + + #[glib::object_subclass] + impl ObjectSubclass for NotifyWindow { + const NAME: &'static str = "NotifyWindow"; + type Type = super::NotifyWindow; + type ParentType = adw::ApplicationWindow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_callbacks(); + + klass.install_action("win.unsubscribe", None, |this, _, _| { + this.unsubscribe(); + }); + klass.install_action("win.show-subscription-info", None, |this, _, _| { + this.show_subscription_info(); + }); + klass.install_action("win.clear-notifications", None, |this, _, _| { + /*spawn_local(this.subscription().clear_notifications().map_err(this.near_toast_overlay().error_handler()));*/ + this.selected_subscription().map(|sub| { + this.spawn_with_near_toast(sub.clear_notifications()); + }); + }); + //klass.bind_template_instance_callbacks(); + } + + // You must call `Widget`'s `init_template()` within `instance_init()`. + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for NotifyWindow { + fn constructed(&self) { + self.parent_constructed(); + let obj = self.obj(); + + // Devel Profile + if PROFILE == "Devel" { + obj.add_css_class("devel"); + } + } + + fn dispose(&self) { + self.dispose_template(); + } + } + + impl WidgetImpl for NotifyWindow {} + impl WindowImpl for NotifyWindow { + // Save window state on delete event + fn close_request(&self) -> glib::Propagation { + if let Err(err) = self.obj().save_window_size() { + warn!(error = %err, "Failed to save window state"); + } + + // Pass close request on to the parent + self.parent_close_request() + } + } + + impl ApplicationWindowImpl for NotifyWindow {} + impl AdwApplicationWindowImpl for NotifyWindow {} +} + +glib::wrapper! { + pub struct NotifyWindow(ObjectSubclass) + @extends gtk::Widget, gtk::Window, adw::Window, adw::ApplicationWindow, + @implements gio::ActionMap, gio::ActionGroup, gtk::Root; +} + +impl NotifyWindow { + pub fn new(app: &NotifyApplication, notifier: system_notifier::Client) -> Self { + let obj: Self = glib::Object::builder().property("application", app).build(); + + if let Err(_) = obj.imp().notifier.set(notifier) { + panic!("setting notifier for first time"); + }; + + // Load latest window state + obj.load_window_size(); + obj.bind_message_list(); + obj.connect_entry_changed(); + obj.connect_items_changed(); + obj.selected_subscription_changed(None); + obj.bind_flag_read(); + + obj + } + fn connect_entry_changed(&self) { + let imp = self.imp(); + let this = self.clone(); + imp.entry.connect_activate(move |entry| { + let p = this + .selected_subscription() + .unwrap() + .publish(entry.text().as_str()); + entry.spawn_with_near_toast(async move { p.await }); + }); + } + fn show_subscription_info(&self) { + let sub = SubscriptionInfoDialog::new(self.selected_subscription().unwrap()); + sub.set_transient_for(Some(self)); + sub.present(); + } + fn connect_items_changed(&self) { + let this = self.clone(); + self.imp() + .subscription_list_model + .connect_items_changed(move |list, _, _, _| { + let imp = this.imp(); + if list.n_items() == 0 { + imp.stack.set_visible_child(&*imp.welcome_view); + } else { + imp.stack.set_visible_child(&*imp.list_view); + } + }); + } + + fn add_subscription(&self, server: &str, topic: &str) { + let mut req = self.notifier().subscribe_request(); + + req.get().set_server(server); + req.get().set_topic(topic); + let res = req.send(); + let this = self.clone(); + self.spawn_with_near_toast(async move { + let imp = this.imp(); + + // Subscription::new will use the pipelined client to retrieve info about the subscription + let subscription = Subscription::new(res.pipeline.get_subscription()); + // We want to still check if there were any errors adding the subscription. + res.promise.await?; + + imp.subscription_list_model.append(&subscription); + let i = imp.subscription_list_model.n_items() - 1; + let row = imp.subscription_list.row_at_index(i as i32); + imp.subscription_list.select_row(row.as_ref()); + Ok::<(), capnp::Error>(()) + }); + } + + fn unsubscribe(&self) { + let mut req = self.notifier().unsubscribe_request(); + + let sub = self.selected_subscription().unwrap(); + + req.get().set_server(&sub.server()); + req.get().set_topic(&sub.topic()); + + let res = req.send(); + let this = self.clone(); + + self.spawn_with_near_toast(async move { + let imp = this.imp(); + res.promise.await?; + + if let Some(i) = imp.subscription_list_model.find(&sub) { + imp.subscription_list_model.remove(i); + } + Ok::<(), capnp::Error>(()) + }); + } + fn notifier(&self) -> &system_notifier::Client { + self.imp().notifier.get().unwrap() + } + fn selected_subscription(&self) -> Option { + let imp = self.imp(); + imp.subscription_list + .selected_row() + .and_then(|row| imp.subscription_list_model.item(row.index() as u32)) + .and_downcast::() + } + fn bind_message_list(&self) { + let imp = self.imp(); + + imp.subscription_list + .bind_model(Some(&imp.subscription_list_model), |obj| { + let sub = obj.downcast_ref::().unwrap(); + + Self::build_subscription_ui(&sub).upcast() + }); + + let this = self.clone(); + imp.subscription_list.connect_row_selected(move |_, _row| { + this.selected_subscription_changed(this.selected_subscription().as_ref()); + }); + + let this = self.clone(); + let req = self.notifier().list_subscriptions_request(); + let res = req.send(); + self.spawn_with_near_toast(async move { + let list = res.promise.await?; + let list = list.get()?.get_list()?; + let imp = this.imp(); + for sub in list { + imp.subscription_list_model.append(&Subscription::new(sub?)); + } + Ok::<(), capnp::Error>(()) + }); + } + fn update_banner(&self, sub: Option<&Subscription>) { + let imp = self.imp(); + if let Some(sub) = sub { + match sub.nice_status() { + Status::Degraded | Status::Down => imp.banner.set_revealed(true), + Status::Up => imp.banner.set_revealed(false), + } + } else { + imp.banner.set_revealed(false); + } + } + fn selected_subscription_changed(&self, sub: Option<&Subscription>) { + let imp = self.imp(); + self.update_banner(sub); + if let Some((sub, id)) = imp.banner_binding.take() { + sub.disconnect(id); + } + if let Some(sub) = sub { + imp.navigation_split_view.set_show_content(true); + imp.message_list + .bind_model(Some(&sub.imp().messages), move |obj| { + let b = obj.downcast_ref::().unwrap(); + let msg = b.borrow::(); + + MessageRow::new(msg.clone()).upcast() + }); + imp.subscription_menu_btn.set_visible(true); + imp.entry.set_sensitive(true); + + let this = self.clone(); + imp.banner_binding.set(Some(( + sub.clone(), + sub.connect_status_notify(move |sub| { + this.update_banner(Some(sub)); + }), + ))); + + let this = self.clone(); + glib::idle_add_local_once(move || { + this.flag_read(); + }); + } else { + imp.message_list + .bind_model(gio::ListModel::NONE, |_| adw::Bin::new().into()); + imp.subscription_menu_btn.set_visible(false); + imp.entry.set_sensitive(false); + } + } + fn flag_read(&self) { + let vadj = self.imp().message_scroll.vadjustment(); + // There is nothing to scroll, so the user viewed all the messages + if vadj.page_size() == vadj.upper() + || ((vadj.page_size() + vadj.value() - vadj.upper()).abs() <= 1.0) + { + self.selected_subscription().map(|sub| { + self.spawn_with_near_toast(sub.flag_all_as_read()); + }); + } + } + fn build_chip(text: &str) -> gtk::Label { + let chip = gtk::Label::new(Some(text)); + chip.add_css_class("chip"); + chip.add_css_class("chip--small"); + chip.set_margin_top(4); + chip.set_margin_bottom(4); + chip.set_margin_start(4); + chip.set_margin_end(4); + chip.set_halign(gtk::Align::Center); + chip.set_valign(gtk::Align::Center); + chip + } + + fn build_subscription_ui(sub: &Subscription) -> impl glib::IsA { + let b = gtk::Box::builder().spacing(8).build(); + + let label = gtk::Label::builder() + .xalign(0.0) + .wrap_mode(gtk::pango::WrapMode::WordChar) + .wrap(true) + .hexpand(true) + .build(); + + sub.bind_property("display-name", &label, "label") + .sync_create() + .build(); + + let counter_chip = Self::build_chip("1+"); + counter_chip.add_css_class("chip--info"); + counter_chip.set_visible(false); + let counter_chip_clone = counter_chip.clone(); + sub.connect_unread_count_notify(move |sub| { + let c = sub.unread_count(); + counter_chip_clone.set_visible(c > 0); + }); + + let status_chip = Self::build_chip("Degraded"); + let status_chip_clone = status_chip.clone(); + + sub.connect_status_notify(move |sub| match sub.nice_status() { + Status::Degraded | Status::Down => { + status_chip_clone.add_css_class("chip--degraded"); + status_chip_clone.set_visible(true); + } + _ => { + status_chip_clone.set_visible(false); + } + }); + + b.append(&counter_chip); + b.append(&label); + b.append(&status_chip); + + b + } + + fn save_window_size(&self) -> Result<(), glib::BoolError> { + let imp = self.imp(); + + let (width, height) = self.default_size(); + + imp.settings.set_int("window-width", width)?; + imp.settings.set_int("window-height", height)?; + + imp.settings + .set_boolean("is-maximized", self.is_maximized())?; + + Ok(()) + } + fn bind_flag_read(&self) { + let imp = self.imp(); + + let this = self.clone(); + imp.message_scroll.connect_edge_reached(move |_, pos_type| { + if pos_type == gtk::PositionType::Bottom { + this.flag_read(); + } + }); + let this = self.clone(); + self.connect_is_active_notify(move |_| { + if this.is_active() { + this.flag_read(); + } + }); + } + + fn load_window_size(&self) { + let imp = self.imp(); + + let width = imp.settings.int("window-width"); + let height = imp.settings.int("window-height"); + let is_maximized = imp.settings.boolean("is-maximized"); + + self.set_default_size(width, height); + + if is_maximized { + self.maximize(); + } + } +} diff --git a/src/window.rs b/src/window.rs deleted file mode 100644 index e17713c..0000000 --- a/src/window.rs +++ /dev/null @@ -1,118 +0,0 @@ -use adw::subclass::prelude::*; -use gtk::prelude::*; -use gtk::{gio, glib}; - -use crate::application::ExampleApplication; -use crate::config::{APP_ID, PROFILE}; - -mod imp { - use super::*; - - #[derive(Debug, gtk::CompositeTemplate)] - #[template(resource = "/com/ranfdev/Notify/ui/window.ui")] - pub struct ExampleApplicationWindow { - #[template_child] - pub headerbar: TemplateChild, - pub settings: gio::Settings, - } - - impl Default for ExampleApplicationWindow { - fn default() -> Self { - Self { - headerbar: TemplateChild::default(), - settings: gio::Settings::new(APP_ID), - } - } - } - - #[glib::object_subclass] - impl ObjectSubclass for ExampleApplicationWindow { - const NAME: &'static str = "ExampleApplicationWindow"; - type Type = super::ExampleApplicationWindow; - type ParentType = adw::ApplicationWindow; - - fn class_init(klass: &mut Self::Class) { - klass.bind_template(); - } - - // You must call `Widget`'s `init_template()` within `instance_init()`. - fn instance_init(obj: &glib::subclass::InitializingObject) { - obj.init_template(); - } - } - - impl ObjectImpl for ExampleApplicationWindow { - fn constructed(&self) { - self.parent_constructed(); - let obj = self.obj(); - - // Devel Profile - if PROFILE == "Devel" { - obj.add_css_class("devel"); - } - - // Load latest window state - obj.load_window_size(); - } - - fn dispose(&self) { - self.dispose_template(); - } - } - - impl WidgetImpl for ExampleApplicationWindow {} - impl WindowImpl for ExampleApplicationWindow { - // Save window state on delete event - fn close_request(&self) -> gtk::Inhibit { - if let Err(err) = self.obj().save_window_size() { - tracing::warn!("Failed to save window state, {}", &err); - } - - // Pass close request on to the parent - self.parent_close_request() - } - } - - impl ApplicationWindowImpl for ExampleApplicationWindow {} - impl AdwApplicationWindowImpl for ExampleApplicationWindow {} -} - -glib::wrapper! { - pub struct ExampleApplicationWindow(ObjectSubclass) - @extends gtk::Widget, gtk::Window, adw::Window, adw::ApplicationWindow, - @implements gio::ActionMap, gio::ActionGroup, gtk::Root; -} - -impl ExampleApplicationWindow { - pub fn new(app: &ExampleApplication) -> Self { - glib::Object::builder().property("application", app).build() - } - - fn save_window_size(&self) -> Result<(), glib::BoolError> { - let imp = self.imp(); - - let (width, height) = self.default_size(); - - imp.settings.set_int("window-width", width)?; - imp.settings.set_int("window-height", height)?; - - imp.settings - .set_boolean("is-maximized", self.is_maximized())?; - - Ok(()) - } - - fn load_window_size(&self) { - let imp = self.imp(); - - let width = imp.settings.int("window-width"); - let height = imp.settings.int("window-height"); - let is_maximized = imp.settings.boolean("is-maximized"); - - self.set_default_size(width, height); - - if is_maximized { - self.maximize(); - } - } -}