commit 7cf731d50992befd535e642cd2e1c05d4c31a588 Author: n0tr1v Date: Wed Jan 19 19:36:14 2022 -0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80a9ea9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +captcha.gif +samples +target +dist +*.log +*.svg +*.env diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b2d6de1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3067 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bhcli" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "clap 3.0.0-beta.4", + "clipboard", + "colors-transform", + "confy", + "crossbeam", + "crossbeam-channel", + "crossterm 0.21.0", + "http", + "image 0.23.14", + "lazy_static", + "linkify", + "rand 0.8.4", + "regex", + "reqwest", + "rodio", + "rpassword", + "select", + "serde", + "serde_derive", + "serde_json", + "termage", + "textwrap 0.14.2", + "toml", + "tui", + "unicode-width", +] + +[[package]] +name = "bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2 1.0.28", + "quote 1.0.9", + "regex", + "rustc-hash", + "shlex", +] + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.43", + "winapi", +] + +[[package]] +name = "clang-sys" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cf2cc85830eae84823884db23c5306442a6c3d5bfd3beb2f2a2c829faa1816" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "3.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim 0.10.0", + "termcolor", + "textwrap 0.14.2", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", +] + +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colors-transform" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9226dbc05df4fb986f48d730b001532580883c4c06c5d1c213f4b34c1c157178" + +[[package]] +name = "combine" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "confy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2913470204e9e8498a0f31f17f90a0de801ae92c8c5ac18c49af4819e6786697" +dependencies = [ + "directories", + "serde", + "toml", +] + +[[package]] +name = "const_fn" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" + +[[package]] +name = "cookie" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +dependencies = [ + "percent-encoding", + "time 0.2.27", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" +dependencies = [ + "cookie", + "idna", + "log", + "publicsuffix", + "serde", + "serde_json", + "time 0.2.27", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "coreaudio-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" +dependencies = [ + "bitflags", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "jni", + "js-sys", + "lazy_static", + "libc", + "mach", + "ndk 0.3.0", + "ndk-glue 0.3.0", + "nix", + "oboe", + "parking_lot", + "stdweb 0.1.3", + "thiserror", + "web-sys", + "winapi", +] + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crossterm" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.28", + "quote 1.0.9", + "strsim 0.9.3", + "syn 1.0.75", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote 1.0.9", + "syn 1.0.75", +] + +[[package]] +name = "deflate" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", +] + +[[package]] +name = "directories" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if 1.0.0", +] + +[[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.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" + +[[package]] +name = "futures-io" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" + +[[package]] +name = "futures-sink" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" + +[[package]] +name = "futures-task" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" + +[[package]] +name = "futures-util" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +dependencies = [ + "autocfg", + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "gif" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" +dependencies = [ + "color_quant", + "lzw", +] + +[[package]] +name = "gif" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "h2" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hound" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" + +[[package]] +name = "html5ever" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", +] + +[[package]] +name = "http" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" + +[[package]] +name = "httpdate" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" + +[[package]] +name = "hyper" +version = "0.14.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f67199e765030fa08fe0bd581af683f0d5bc04ea09c2b1102012c5fb90e7fd" +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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebdff791af04e30089bde8ad2a632b86af433b40c04db8d70ad4b21487db7a6a" +dependencies = [ + "byteorder", + "gif 0.10.3", + "jpeg-decoder", + "lzw", + "num-derive 0.2.5", + "num-iter", + "num-rational 0.1.42", + "num-traits", + "png 0.12.0", + "scoped_threadpool", +] + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif 0.11.2", + "jpeg-decoder", + "num-iter", + "num-rational 0.3.2", + "num-traits", + "png 0.16.8", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "inflate" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +dependencies = [ + "adler32", +] + +[[package]] +name = "instant" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" +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 = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + +[[package]] +name = "libc" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" + +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "linkify" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d828fdc1ffceb369a5a9183bd4df2dbb3678f40c8b3fbaa9231de32beb29f9" +dependencies = [ + "memchr", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "lzw" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" +dependencies = [ + "minimp3-sys", + "slice-deque", + "thiserror", +] + +[[package]] +name = "minimp3-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" +dependencies = [ + "cc", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.3.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-glue" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.4.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate 0.1.5", + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", +] + +[[package]] +name = "ndk-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.4.0", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e97c412795abf6c24ba30055a8f20642ea57ca12875220b854cfa501bf1e48" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +dependencies = [ + "proc-macro-crate 1.0.0", + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", +] + +[[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 = "oboe" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" +dependencies = [ + "jni", + "ndk 0.4.0", + "ndk-glue 0.4.0", + "num-derive 0.3.3", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc" +dependencies = [ + "cc", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "openssl" +version = "0.10.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "png" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54b9600d584d3b8a739e1662a595fab051329eff43f20e7d8cc22872962145b" +dependencies = [ + "bitflags", + "deflate 0.7.20", + "inflate", + "num-iter", +] + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate 0.8.6", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +dependencies = [ + "thiserror", + "toml", +] + +[[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 1.0.28", + "quote 1.0.9", + "syn 1.0.75", + "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 1.0.28", + "quote 1.0.9", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid 0.2.2", +] + +[[package]] +name = "publicsuffix" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b4ce31ff0a27d93c8de1849cf58162283752f065a90d508f1105fa6c9a213f" +dependencies = [ + "idna", + "url", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2 1.0.28", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.3", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_urlencoded", + "time 0.2.27", + "tokio", + "tokio-native-tls", + "tokio-socks", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rodio" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "minimp3", +] + +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9bd29cdffb8875b04f71c51058f940cf4e390bbfd2ce669c4f22cd70b492a5" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "num", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19133a286e494cc3311c165c4676ccb1fd47bed45b55f9d71fbd784ad4cea6f8" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "select" +version = "0.6.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b51951aa2af52d64920acfe8b0df8d8f4071811a9e575db56a037dd5ee3b894" +dependencies = [ + "bit-set", + "html5ever", + "markup5ever_rcdom", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", +] + +[[package]] +name = "serde_json" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "signal-hook" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1" + +[[package]] +name = "slab" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" + +[[package]] +name = "slice-deque" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" +dependencies = [ + "libc", + "mach", + "winapi", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "socket2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "serde", + "serde_derive", + "syn 1.0.75", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2 1.0.28", + "quote 1.0.9", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn 1.0.75", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2 1.0.28", + "quote 1.0.9", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "unicode-xid 0.2.2", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tendril" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termage" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462dc01ca2f07b9bacbd0dc2411f1cb810b6d615d530d72300980a2a1e1347bd" +dependencies = [ + "clap 2.33.3", + "image 0.19.0", + "terminal_graphics", + "terminal_size", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_graphics" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3585912e123cb72d56d923dde7419b329481d4ed76cb01d8c0d733281484edb" + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", +] + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb 0.4.20", + "time-macros", + "version_check", + "winapi", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2 1.0.28", + "quote 1.0.9", + "standback", + "syn 1.0.75", +] + +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "winapi", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tui" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.20.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" + +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fded345a6559c2cfee778d562300c581f7d4ff3edb9b0d230d69800d213972" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" +dependencies = [ + "quote 1.0.9", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.75", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" + +[[package]] +name = "web-sys" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" + +[[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-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[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 = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] + +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "xml5ever" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59" +dependencies = [ + "log", + "mac", + "markup5ever", + "time 0.1.43", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bf32192 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "bhcli" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base64 = "0.13.0" +chrono = "0.4.19" +clap = "3.0.0-beta.4" +clipboard = "0.5.0" +colors-transform = "0.2.4" +confy = "0.4.0" +crossbeam = "0.8.1" +crossbeam-channel = "0.5.1" +crossterm = { version = "0.21.0" } +http = "0.2.4" +image = "0.23.14" +lazy_static = "1.4.0" +linkify = "0.7.0" +rand = "0.8.4" +regex = "1.5.4" +reqwest = { version = "0.11.4", features = ["blocking", "cookies", "socks", "multipart"] } +rodio = "0.14.0" +rpassword = "5.0.1" +select = "0.6.0-alpha.1" +serde = "1.0.130" +serde_derive = "1.0.88" +serde_json = "1.0" +termage = "1.1.1" +textwrap = "0.14.2" +toml = "0.5.8" +tui = { version = "0.16.0", features = ["crossterm"], default-features = false } +unicode-width = "0.1.8" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2986467 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM rust:1.54 as bhcli-builder +RUN apt-get update +RUN apt-get install -y pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev libx11-dev diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3a90a03 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +PWD = $(shell pwd) + +build-docker-bin: + docker run --rm -it -v $(PWD):/Documents/bhcli -w /Documents/bhcli bhcli sh -c \ + 'CARGO_TARGET_DIR=./target/linux cargo build --release' + +build-darwin: + cargo build --release + cp target/release/bhcli dist/bhcli.darwin.amd64 + tar -czvf dist/bhcli.darwin.amd64.tar.gz dist/bhcli.darwin.amd64 + openssl dgst -sha256 dist/bhcli.darwin.amd64.tar.gz | cut -d ' ' -f 2 > dist/bhcli.darwin.amd64.tar.gz.checksum + rm dist/bhcli.darwin.amd64 + +build-linux: build-docker-bin + cp target/linux/release/bhcli dist/bhcli.linux.amd64 + tar -czvf dist/bhcli.linux.amd64.tar.gz dist/bhcli.linux.amd64 + openssl dgst -sha256 dist/bhcli.linux.amd64.tar.gz | cut -d ' ' -f 2 > dist/bhcli.linux.amd64.tar.gz.checksum + rm dist/bhcli.linux.amd64 + +cross-compile-windows: + cargo build --release --target x86_64-pc-windows-gnu + cp target/x86_64-pc-windows-gnu/release/bhcli.exe dist/bhcli.windows.amd64.exe + zip dist/bhcli.windows.amd64.zip dist/bhcli.windows.amd64.exe + openssl dgst -sha256 dist/bhcli.windows.amd64.zip | cut -d ' ' -f 2 > dist/bhcli.windows.amd64.zip.checksum + rm dist/bhcli.windows.amd64.exe + +process-windows: + zip dist/bhcli.windows.amd64.zip dist/bhcli.exe + openssl dgst -sha256 dist/bhcli.windows.amd64.zip | cut -d ' ' -f 2 > dist/bhcli.windows.amd64.zip.checksum + rm dist/bhcli.exe + +rsync: + rsync --recursive --times --compress --progress dist/ torsv:/root/dist/downloads-bhcli + +deploy: build-darwin cross-compile-windows build-linux rsync + +.PHONY: build-darwin process-windows cross-compile-windows rsync diff --git a/README.md b/README.md new file mode 100644 index 0000000..5933368 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# BHCLI + +![screenshot](screenshot.png "Screenshot") + +## Description + +This is a CLI client for any of [le-chat-php](https://github.com/DanWin/le-chat-php) +currently supported chats are [Black Hat Chat](http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion) and +[Daniel's chat](http://danschat356lctri3zavzh6fbxg2a7lo6z3etgkctzzpspewu7zdsaqd.onion) + +## Pre-built binaries + +Pre-buit binaries can be found on the [official website](http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/bhcli) + +## Features + +- Sound notifications when tagged/pmmed +- Private messages `/pm username message` +- Kick someone `/kick username message` | `/k username message` +- Delete last message `/dl` +- Delete last X message `/dl5` will delete the last 5 messages +- Delete all messages `/dall` +- Ignore someone `/ignore username` +- Unignore someone `/unignore username` +- Toggle notifications sound `m` +- Toggle a "guest" view, by filtering out PMs and "Members chat" `shift+G` +- Filter messages `/f terms` +- Copy a selected message to clipboard `ctrl+C` | `y` +- Copy the first link in a message to clipboard `shift+Y` +- Directly tag author of selected message `t` will prefil the input with `@username ` +- Directly private message author of selected message `p` will prefil the input with `/pm username ` +- Shortcut to kick author of selected message `ctrl+k` will prefil the input with `/kick username ` +- captcha is displayed directly in terminal 10 times the real size +- Upload file `/u C:\path\to\file.png @username message` (@username is optional) `@members` for members group +- `` to autocomplete usernames while typing + +### Editing mode +- `ctrl+A` Move cursor to start of line +- `ctrl+E` Move cursor to end of line +- `ctrl+F` Move cursor a word forward +- `ctrl+B` Move cursor a word backward + +### Messages navigation +- Page down the messages list `ctrl+D` | `page down` +- Page up the messages list `ctrl+U` | `page up` +- Going down 1 message `j` | `down arrow` +- Going up 1 message `k` | `up arrow` + +## Build from source + +### Windows + +- Install C++ build tools https://visualstudio.microsoft.com/visual-cpp-build-tools/ +- Install Rust https://www.rust-lang.org/learn/get-started +- Download & extract code source +- Compile with `cargo build --release` + +### OSx + +- Install Rust https://www.rust-lang.org/learn/get-started +- Compile with `cargo build --release` + +### Linux + +- Install Rust +- Install dependencies `apt-get install -y pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev libx11-dev` +- Compile with `cargo build --release` + +## Cross compile + +`cargo build --release --target x86_64-pc-windows-gnu` diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..9b80e71 Binary files /dev/null and b/screenshot.png differ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..22122a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3042 @@ +mod util; + +use base64::decode; +use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; +use clap::{AppSettings, Clap}; +use clipboard::ClipboardContext; +use clipboard::ClipboardProvider; +use colors_transform::{Color, Rgb}; +use crossbeam_channel::{self, after, select, Select}; +use crossterm::event; +use crossterm::event::Event as CEvent; +use crossterm::event::{MouseEvent, MouseEventKind}; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use http::StatusCode; +use image; +use image::GenericImageView; +use lazy_static::lazy_static; +use linkify::LinkFinder; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use regex::Regex; +use reqwest::blocking::multipart; +use reqwest::blocking::Client; +use rodio::{source::Source, Decoder, OutputStream}; +use select::document::Document; +use select::predicate::{And, Attr, Name}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::error; +use std::fs; +use std::io::Cursor; +use std::io::{self, Write}; +use std::process; +use std::sync::Arc; +use std::sync::Mutex; +use std::thread; +use std::time; +use std::time::Duration; +use std::time::Instant; +use termage; +use textwrap; +use tui::layout::Rect; +use tui::style::Color as tuiColor; +use tui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + style::{Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, Borders, List, ListItem, Paragraph}, + Frame, Terminal, +}; +use unicode_width::UnicodeWidthStr; +use util::StatefulList; + +const LANG: &str = "en"; +const SEND_TO_ALL: &str = "s *"; +const SEND_TO_MEMBERS: &str = "s ?"; +const SEND_TO_STAFFS: &str = "s %"; +const SEND_TO_ADMINS: &str = "s _"; +const SOUND1: &[u8] = include_bytes!("sound1.mp3"); +const DKF_URL: &str = "http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion"; +const BHCLI_BLOG_URL: &str = + "http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/bhcli"; +const BAN_IMPOSTERS: bool = true; +const SERVER_DOWN_500_ERR: &str = "500 Internal Server Error, server down"; +const SERVER_DOWN_ERR: &str = "502 Bad Gateway, server down"; +const KICKED_ERR: &str = "You have been kicked"; +const REG_ERR: &str = "This nickname is a registered member"; +const NICKNAME_ERR: &str = "Invalid nickname"; +const CAPTCHA_WG_ERR: &str = "Wrong Captcha"; +const CAPTCHA_USED_ERR: &str = "Captcha already used or timed out"; +const UNKNOWN_ERR: &str = "Unknown error"; +const N0TR1V: &str = "n0tr1v"; +const DNMX_URL: &str = "http://hxuzjtocnzvv5g2rtg2bhwkcbupmk7rclb6lly3fo4tvqkk5oyrv3nid.onion"; + +type Result = std::result::Result>; + +lazy_static! { + static ref SESSION_RGX: Regex = Regex::new(r#"session=([^&]+)"#).unwrap(); + static ref COLOR_RGX: Regex = Regex::new(r#"color:\s*([#\w]+)\s*;"#).unwrap(); + static ref COLOR1_RGX: Regex = Regex::new(r#"^#([0-9A-Fa-f]{6})$"#).unwrap(); + static ref PM_RGX: Regex = Regex::new(r#"^/pm ([^\s]+) (.*)"#).unwrap(); + static ref KICK_RGX: Regex = Regex::new(r#"^/(?:kick|k) ([^\s]+)\s?(.*)"#).unwrap(); + static ref IGNORE_RGX: Regex = Regex::new(r#"^/ignore ([^\s]+)"#).unwrap(); + static ref UNIGNORE_RGX: Regex = Regex::new(r#"^/unignore ([^\s]+)"#).unwrap(); + static ref DLX_RGX: Regex = Regex::new(r#"^/dl([\d]+)$"#).unwrap(); + static ref UPLOAD_RGX: Regex = Regex::new(r#"^/u\s([^\s]+)\s?(?:@([^\s]+)\s)?(.*)$"#).unwrap(); + static ref FIND_RGX: Regex = Regex::new(r#"^/f\s(.*)$"#).unwrap(); + static ref NEW_NICKNAME_RGX: Regex = Regex::new(r#"^/nick\s(.*)$"#).unwrap(); + static ref NEW_COLOR_RGX: Regex = Regex::new(r#"^/color\s(.*)$"#).unwrap(); +} + +#[derive(Debug, Serialize, Deserialize)] +enum Typ { + BHC, + Custom, +} + +impl Typ { + fn bhc() -> Self { + Typ::BHC + } +} + +fn default_empty_str() -> String { + "".to_string() +} + +#[derive(Debug, Serialize, Deserialize)] +struct Profile { + username: String, + password: String, + #[serde(default = "Typ::bhc")] + typ: Typ, + #[serde(default = "default_empty_str")] + url: String, + #[serde(default = "default_empty_str")] + date_format: String, + #[serde(default = "default_empty_str")] + page_php: String, + #[serde(default = "default_empty_str")] + members_tag: String, + #[serde(default = "default_empty_str")] + keepalive_send_to: String, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +struct MyConfig { + dkf_api_key: Option, + profiles: HashMap, +} + +#[derive(Clap)] +#[clap( + name = "bhcli", + version = "0.0.1", + author = "n0tr1v " +)] +#[clap(setting = AppSettings::ColoredHelp)] +struct Opts { + #[clap(long, env = "DKF_API_KEY")] + dkf_api_key: Option, + #[clap(short, long, env = "BHC_USERNAME")] + username: Option, + #[clap(short, long, env = "BHC_PASSWORD")] + password: Option, + #[clap(short, long, env = "BHC_MANUAL_CAPTCHA")] + manual_captcha: bool, + #[clap(short, long, env = "BHC_GUEST_COLOR")] + guest_color: Option, + #[clap(short, long, env = "BHC_REFRESH_RATE", default_value = "5")] + refresh_rate: u64, + #[clap(long, env = "BHC_MAX_LOGIN_RETRY", default_value = "5")] + max_login_retry: isize, + #[clap(long)] + url: Option, + #[clap(long)] + page_php: Option, + #[clap(long)] + datetime_fmt: Option, + #[clap(long)] + members_tag: Option, + #[clap(short, long)] + dan: bool, + #[clap( + short, + long, + env = "BHC_PROXY_URL", + default_value = "socks5h://127.0.0.1:9050" + )] + socks_proxy_url: String, + #[clap(long, env = "DNMX_USERNAME")] + dnmx_username: Option, + #[clap(long, env = "DNMX_PASSWORD")] + dnmx_password: Option, + #[clap(short = 'c', long, default_value = "default")] + profile: String, +} + +struct LeChatPHPConfig { + url: String, + datetime_fmt: String, + page_php: String, + keepalive_send_to: Option, + members_tag: String, + staffs_tag: String, +} + +impl LeChatPHPConfig { + fn new_black_hat_chat_config() -> Self { + Self { + url: "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion".to_owned(), + datetime_fmt: "%m-%d %H:%M:%S".to_owned(), + page_php: "index.php".to_owned(), + keepalive_send_to: Some("0".to_owned()), + members_tag: "[M] ".to_owned(), + staffs_tag: "[Staff] ".to_owned(), + } + } + + fn new_dans_chat_config() -> Self { + Self { + url: "http://danschat356lctri3zavzh6fbxg2a7lo6z3etgkctzzpspewu7zdsaqd.onion".to_owned(), + datetime_fmt: "%d-%m %H:%M:%S".to_owned(), + page_php: "chat.php".to_owned(), + keepalive_send_to: None, + members_tag: "[Members] ".to_owned(), + staffs_tag: "[Staff] ".to_owned(), + } + } +} + +struct BaseClient { + username: String, + password: String, +} + +struct LeChatPHPClient<'a> { + base_client: BaseClient, + guest_color: String, + client: &'a Client, + session: String, + config: LeChatPHPConfig, + dkf_api_key: Option, + manual_captcha: bool, + refresh_rate: u64, + max_login_retry: isize, + + is_muted: Arc>, + show_sys: bool, + display_guest_view: bool, + display_hidden_msgs: bool, + tx: crossbeam_channel::Sender, + rx: Arc>>, + + color_tx: crossbeam_channel::Sender<()>, + color_rx: Arc>>, +} + +impl<'a> LeChatPHPClient<'a> { + fn run_forever(&mut self) { + let max_retry = self.max_login_retry; + let mut attempt = 0; + loop { + if let Err(e) = self.login() { + if e.to_string() == KICKED_ERR + || e.to_string() == REG_ERR + || e.to_string() == NICKNAME_ERR + || e.to_string() == UNKNOWN_ERR + { + eprintln!("{:?}", e.to_string()); + break; + } else if e.to_string() == CAPTCHA_WG_ERR || e.to_string() == CAPTCHA_USED_ERR { + } else if e.to_string() == SERVER_DOWN_ERR || e.to_string() == SERVER_DOWN_500_ERR { + eprintln!("{}", e.to_string()); + } else if let Some(err) = e.downcast_ref::() { + if err.is_connect() { + eprintln!("{:?}\nIs tor proxy enabled ?", err.to_string()); + break; + } else if err.is_timeout() { + eprintln!("timeout: {:?}", err.to_string()); + } else { + eprintln!("{:?}", err.to_string()); + } + } else { + eprintln!("unknown error: {:?}", e.to_string()); + } + } else { + attempt = 0; + match self.get_msgs() { + Ok(ExitSignal::NeedLogin) => {} + Ok(ExitSignal::Terminate) => return, + Err(e) => eprintln!("{:?}", e), + } + } + attempt += 1; + if max_retry > 0 && attempt > max_retry { + break; + } + self.session = "".to_owned(); + let retry_in = time::Duration::from_secs(2); + let mut msg = format!("retry login in {:?}, attempt: {}", retry_in, attempt); + if max_retry > 0 { + msg += &format!("/{}", max_retry); + } + println!("{}", msg); + thread::sleep(retry_in); + } + } + + fn start_keepalive_thread( + &self, + exit_rx: crossbeam_channel::Receiver, + last_post_rx: crossbeam_channel::Receiver, + ) -> thread::JoinHandle<()> { + let tx = self.tx.clone(); + let send_to = self.config.keepalive_send_to.clone(); + thread::spawn(move || loop { + let timeout = after(time::Duration::from_secs(60 * 75)); + select! { + // Whenever we send a message to chat server, + // we will receive a message on this channel + // and reset the timer for next keepalive. + recv(&last_post_rx) -> _ => {}, + recv(&exit_rx) -> _ => return, + recv(&timeout) -> _ => { + tx.send(PostType::Post("".to_owned(), send_to.clone())).unwrap(); + tx.send(PostType::DeleteLast).unwrap(); + }, + } + }) + } + + // Thread that POST to chat server + fn start_post_msg_thread( + &self, + exit_rx: crossbeam_channel::Receiver, + last_post_tx: crossbeam_channel::Sender, + ) -> thread::JoinHandle<()> { + let client = self.client.clone(); + let rx = Arc::clone(&self.rx); + let full_url = format!("{}/{}", &self.config.url, &self.config.page_php); + let url = format!("{}?action=post&session={}", &full_url, self.session); + let session = self.session.clone(); + thread::spawn(move || loop { + let rx = rx.lock().unwrap(); + + let mut sel = Select::new(); + let oper1 = sel.recv(&rx); + let oper2 = sel.recv(&exit_rx); + let oper = sel.select(); + + if oper.index() == oper2 { + if let Ok(_) = oper.recv(&exit_rx) { + return; + } + } else if oper.index() == oper1 { + if let Ok(post_type_recv) = oper.recv(&rx) { + loop { + let post_type = post_type_recv.clone(); + let resp = match client.get(url.clone()).send() { + Ok(r) => r, + Err(e) => { + eprintln!("failed to send request: {:?}", e); + continue; + } + }; + let resp_text = resp.text().unwrap(); + let doc = Document::from(resp_text.as_str()); + let nc = doc.select(Attr("name", "nc")).next().unwrap(); + let nc_value = nc.attr("value").unwrap().to_owned(); + let postid = doc.select(Attr("name", "postid")).next().unwrap(); + let postid_value = postid.attr("value").unwrap().to_owned(); + let mut params: Vec<(&str, String)> = vec![ + ("lang", LANG.to_owned()), + ("nc", nc_value.to_owned()), + ("session", session.clone()), + ]; + + if let PostType::Clean(date, text) = post_type { + if let Err(_) = + delete_message(&client, &full_url, &mut params, date, text) + { + continue; + } + break; + } + + let mut req = client.post(&full_url); + let mut form: Option = None; + + match post_type { + PostType::Post(msg, send_to) => { + params.extend(vec![ + ("action", "post".to_owned()), + ("postid", postid_value.to_owned()), + ("message", msg.clone()), + ("sendto", send_to.unwrap_or(SEND_TO_ALL.to_owned())), + ]); + } + PostType::NewNickname(new_nickname) => { + if let Err(e) = + set_profile_base_info(&client, &full_url, &mut params) + { + eprintln!("{:?}", e); + continue; + } + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("newnickname", new_nickname), + ]); + } + PostType::NewColor(new_color) => { + if let Err(e) = + set_profile_base_info(&client, &full_url, &mut params) + { + eprintln!("{:?}", e); + continue; + } + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("colour", new_color), + ]); + } + PostType::Ignore(username) => { + if let Err(e) = + set_profile_base_info(&client, &full_url, &mut params) + { + eprintln!("{:?}", e); + continue; + } + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("ignore", username), + ]); + } + PostType::Unignore(username) => { + if let Err(e) = + set_profile_base_info(&client, &full_url, &mut params) + { + eprintln!("{:?}", e); + continue; + } + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("unignore", username), + ]); + } + PostType::Profile(new_color, new_nickname) => { + if let Err(e) = + set_profile_base_info(&client, &full_url, &mut params) + { + eprintln!("{:?}", e); + continue; + } + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("colour", new_color), + ("newnickname", new_nickname), + ]); + } + PostType::Kick(msg, send_to) => { + params.extend(vec![ + ("action", "post".to_owned()), + ("postid", postid_value.to_owned()), + ("message", msg), + ("sendto", send_to), + ("kick", "kick".to_owned()), + ("what", "purge".to_owned()), + ]); + } + PostType::DeleteLast | PostType::DeleteAll => { + params.extend(vec![("action", "delete".to_owned())]); + if let PostType::DeleteAll = post_type { + params.extend(vec![ + ("sendto", SEND_TO_ALL.to_owned()), + ("confirm", "yes".to_owned()), + ("what", "all".to_owned()), + ]); + } else { + params.extend(vec![ + ("sendto", "".to_owned()), + ("what", "last".to_owned()), + ]); + } + } + PostType::Upload(file_path, send_to, msg) => { + form = Some( + multipart::Form::new() + .text("lang", LANG.to_owned()) + .text("nc", nc_value.to_owned()) + .text("session", session.clone()) + .text("action", "post".to_owned()) + .text("postid", postid_value.to_owned()) + .text("message", msg) + .text("sendto", send_to.to_owned()) + .text("what", "purge".to_owned()) + .file("file", file_path) + .unwrap(), + ); + } + PostType::Clean(_, _) => {} + } + + if let Some(form_content) = form { + req = req.multipart(form_content); + } else { + req = req.form(¶ms); + } + if let Err(err) = req.send() { + if err.is_timeout() { + eprintln!("{:?}", err.to_string()); + continue; + } else { + eprintln!("{:?}", err.to_string()); + } + } + break; + } + last_post_tx.send(true).unwrap(); + } + } + }) + } + + // Thread that update messages every "refresh_rate" + fn start_get_msgs_thread( + &self, + sig: &Arc>, + messages: &Arc>>, + users: &Arc>, + messages_updated_tx: crossbeam_channel::Sender, + ) -> thread::JoinHandle<()> { + let client = self.client.clone(); + let messages = Arc::clone(&messages); + let users = Arc::clone(&users); + let session = self.session.clone(); + let username = self.base_client.username.clone(); + let refresh_rate = self.refresh_rate.clone(); + let base_url = self.config.url.clone(); + let page_php = self.config.page_php.clone(); + let datetime_fmt = self.config.datetime_fmt.clone(); + let is_muted = Arc::clone(&self.is_muted); + let exit_rx = sig.lock().unwrap().clone(); + let sig = Arc::clone(sig); + let tx = self.tx.clone(); + let members_tag = self.config.members_tag.clone(); + let h = thread::spawn(move || loop { + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); + + let url = format!( + "{}/{}?action=view&session={}&lang={}", + base_url, page_php, session, LANG + ); + if let Ok(resp) = client.get(url).send() { + if let Ok(resp_text) = resp.text() { + let resp_text = resp_text.replace("
", "\n"); + let doc = Document::from(resp_text.as_str()); + let mut should_notify = false; + { + let mut messages = messages.lock().unwrap(); + if let Ok(new_messages) = extract_messages(&doc) { + let parse_date = |date: &str| -> NaiveDateTime { + let now = chrono::offset::Utc::now(); + let date_fmt = format!("%Y-{}", datetime_fmt); + NaiveDateTime::parse_from_str( + format!("{}-{}", now.year(), date).as_str(), + date_fmt.as_str(), + ) + .unwrap() + }; + + if let Some(last_known_msg) = messages.get(0) { + let msg = last_known_msg; + let parsed_dt = parse_date(&msg.date); + for new_msg in &new_messages { + let new_parsed_dt = parse_date(&new_msg.date); + + if parsed_dt > new_parsed_dt + || (new_msg.date == msg.date && msg.text == new_msg.text) + { + break; + } + + if let Some((from, to_opt, msg)) = + get_message(&new_msg.text, &members_tag) + { + // Process new messages + + // !bhcli filters + if msg == "!bhcli" && username == N0TR1V { + let msg = format!("@{} -> {}", from, BHCLI_BLOG_URL) + .to_owned(); + tx.send(PostType::Post(msg, None)).unwrap(); + } else if msg == "/logout" + && from == "STUXNET" + && username == N0TR1V + { + eprintln!("forced logout by {}", from); + sig.lock().unwrap().signal(ExitSignal::Terminate); + return; + } + // Notify when tagged + if msg.contains(format!("@{}", &username).as_str()) { + should_notify = true; + } + // Notify when PM is received + if let Some(to) = to_opt { + if to == username && msg != "!up" { + should_notify = true; + } + } + } + } + } + + // Build messages vector. Tag deleted messages. + let mut msgs_repl = Vec::new(); + let mut old_msg_ptr = 0; + let mut new_msg_ptr = 0; + let mut i = 0; + while old_msg_ptr < messages.len() || new_msg_ptr < new_messages.len() { + if let Some(old_msg) = messages.get(old_msg_ptr) { + if let Some(new_msg) = new_messages.get(new_msg_ptr) { + let new_parsed_dt = parse_date(&new_msg.date); + let parsed_dt = parse_date(&old_msg.date); + if new_parsed_dt > parsed_dt { + msgs_repl.push(new_msg.clone()); + new_msg_ptr += 1; + } else if new_parsed_dt == parsed_dt { + if old_msg.text.text() == new_msg.text.text() { + msgs_repl.push(old_msg.clone()); + } else { + msgs_repl.push(new_msg.clone()); + } + new_msg_ptr += 1; + old_msg_ptr += 1; + } else { + let mut tmp = old_msg.clone(); + tmp.deleted = true; + msgs_repl.push(tmp); + old_msg_ptr += 1; + } + } else { + msgs_repl.push(old_msg.clone()); + old_msg_ptr += 1; + } + } else if let Some(new_msg) = new_messages.get(new_msg_ptr) { + msgs_repl.push(new_msg.clone()); + new_msg_ptr += 1; + } + i += 1; + if i > 2000 { + break; + } + } + + // Notify new messages has arrived. + // This ensure that we redraw the messages on the screen right away. + // Otherwise, the screen would not redraw until a keyboard event occurs. + messages_updated_tx.send(true).unwrap(); + // Update "messages" with new value + *messages = msgs_repl; + } else { + // Failed to get messages, probably need relogin + sig.lock().unwrap().signal(ExitSignal::NeedLogin); + return; + } + } + let muted = { *is_muted.lock().unwrap() }; + if should_notify && !muted { + if let Err(err) = stream_handle.play_raw(source.convert_samples()) { + eprintln!("{}", err); + } + } + { + let mut users = users.lock().unwrap(); + ban_imposters(&tx, &username, &users); + *users = extract_users(&doc); + } + } + } + + let timeout = after(time::Duration::from_secs(refresh_rate)); + select! { + recv(&exit_rx) -> _ => return, + recv(&timeout) -> _ => {}, + } + }); + h + } + + fn get_msgs(&mut self) -> Result { + let terminate_signal: ExitSignal; + + let messages: Arc>> = Arc::new(Mutex::new(Vec::new())); + let users: Arc> = Arc::new(Mutex::new(Users::default())); + + // Create default app state + let mut app = App::default(); + + // Each threads gets a clone of the receiver. + // When someone calls ".signal", all threads recieve it, + // and knows that they have to terminate. + let sig = Arc::new(Mutex::new(Sig::new())); + + let (messages_updated_tx, messages_updated_rx) = crossbeam_channel::unbounded(); + let (last_post_tx, last_post_rx) = crossbeam_channel::unbounded(); + + let h1 = self.start_keepalive_thread(sig.lock().unwrap().clone(), last_post_rx); + let h2 = self.start_post_msg_thread(sig.lock().unwrap().clone(), last_post_tx); + let h3 = self.start_get_msgs_thread(&sig, &messages, &users, messages_updated_tx); + + // Terminal initialization + let mut stdout = io::stdout(); + enable_raw_mode().unwrap(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Setup event handlers + let (events, h4) = Events::with_config(Config { + messages_updated_rx, + exit_rx: sig.lock().unwrap().clone(), + tick_rate: time::Duration::from_millis(250), + }); + + loop { + { + app.is_muted = *self.is_muted.lock().unwrap(); + app.show_sys = self.show_sys; + app.display_guest_view = self.display_guest_view; + app.display_hidden_msgs = self.display_hidden_msgs; + app.members_tag = self.config.members_tag.clone(); + app.staffs_tag = self.config.staffs_tag.clone(); + } + // Draw UI + terminal.draw(|f| { + draw_terminal_frame(f, &mut app, &messages, &users); + })?; + + // Handle input + match self.handle_input(&events, &mut app, &messages, &users) { + Err(ExitSignal::Terminate) => { + terminate_signal = ExitSignal::Terminate; + sig.lock().unwrap().signal(terminate_signal.clone()); + break; + } + Err(ExitSignal::NeedLogin) => { + terminate_signal = ExitSignal::NeedLogin; + sig.lock().unwrap().signal(terminate_signal.clone()); + break; + } + Ok(_) => continue, + }; + } + + // Cleanup before leaving + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + terminal.clear()?; + terminal.set_cursor(0, 0)?; + + h1.join().unwrap(); + h2.join().unwrap(); + h3.join().unwrap(); + h4.join().unwrap(); + + Ok(terminate_signal) + } + + fn post_msg(&self, post_type: PostType) -> Result<()> { + self.tx.send(post_type)?; + Ok(()) + } + + fn login(&mut self) -> Result<()> { + // If we provided a session, skip login process + if self.session != "" { + return Ok(()); + } + + // Get login page + let login_url = format!("{}/{}", &self.config.url, &self.config.page_php); + let resp = self.client.get(&login_url).send()?; + if resp.status() == StatusCode::BAD_GATEWAY { + return Err(SERVER_DOWN_ERR.into()); + } + let resp = resp.text()?; + let doc = Document::from(resp.as_str()); + + // Post login form + let mut params = vec![ + ("action", "login".to_owned()), + ("lang", LANG.to_owned()), + ("nick", self.base_client.username.clone()), + ("pass", self.base_client.password.clone()), + ("colour", self.guest_color.clone()), + ]; + + if let Some(captcha_value) = doc + .select(And(Name("input"), Attr("name", "challenge"))) + .next() + { + let captcha_value = captcha_value.attr("value").unwrap(); + + let mut captcha_input = String::new(); + if self.manual_captcha { + let captcha_img = doc.select(Name("img")).next().unwrap().attr("src").unwrap(); + + if let Some(dkf_api_key) = self.dkf_api_key.clone() { + // If we have the DKF_API_KEY, auto solve captcha using the api + let params = vec![("captcha", captcha_img)]; + let resp = self + .client + .post(format!("{}/api/v1/captcha/solver", DKF_URL)) + .header("DKF_API_KEY", dkf_api_key) + .form(¶ms) + .send()?; + let resp = resp.text()?; + let rgx = Regex::new(r#""answer": "([^"]+)""#)? + .captures(resp.as_str()) + .unwrap(); + let answer = rgx.get(1).unwrap().as_str(); + captcha_input = answer.to_owned(); + } else { + // Otherwise, save the captcha on disk and prompt user for answer + let img_decoded = + decode(captcha_img.strip_prefix("data:image/gif;base64,").unwrap())?; + let img = image::load_from_memory(&img_decoded).unwrap(); + let img_buf = image::imageops::resize( + &img, + img.width() * 4, + img.height() * 4, + image::imageops::FilterType::Nearest, + ); + // Save captcha as file on disk + img_buf.save("captcha.gif").unwrap(); + + termage::display_image("captcha.gif", img.width(), img.height()); + + // Enter captcha + print!("captcha: "); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut captcha_input).unwrap(); + trim_newline(&mut captcha_input); + } + } else { + // Captcha is not actually required for memebers (BHC) + captcha_input = "12345".to_owned(); + } + + params.extend(vec![ + ("challenge", captcha_value.to_owned()), + ("captcha", captcha_input.clone()), + ]); + } + + let resp = self.client.post(&login_url).form(¶ms).send()?; + match resp.status() { + StatusCode::BAD_GATEWAY => return Err(SERVER_DOWN_ERR.into()), + StatusCode::INTERNAL_SERVER_ERROR => return Err(SERVER_DOWN_500_ERR.into()), + _ => {} + } + let mut resp = resp.text()?; + if resp.contains(CAPTCHA_USED_ERR) { + return Err(CAPTCHA_USED_ERR.into()); + } else if resp.contains(CAPTCHA_WG_ERR) { + return Err(CAPTCHA_WG_ERR.into()); + } else if resp.contains(REG_ERR) { + return Err(REG_ERR.into()); + } else if resp.contains(NICKNAME_ERR) { + return Err(NICKNAME_ERR.into()); + } else if resp.contains(KICKED_ERR) { + return Err(KICKED_ERR.into()); + } + + let mut doc = Document::from(resp.as_str()); + if let Some(body) = doc.select(Name("body")).next() { + if let Some(body_class) = body.attr("class") { + if body_class == "error" { + if let Some(h2) = doc.select(Name("h2")).next() { + eprintln!("{}", h2.text()); + } + return Err(UNKNOWN_ERR.into()); + } else if body_class == "failednotice" { + eprintln!("failed logins: {}", body.text()); + let nc = doc.select(Attr("name", "nc")).next().unwrap(); + let nc_value = nc.attr("value").unwrap().to_owned(); + let params: Vec<(&str, String)> = vec![ + ("lang", LANG.to_owned()), + ("nc", nc_value.to_owned()), + ("action", "login".to_owned()), + ]; + resp = self.client.post(&login_url).form(¶ms).send()?.text()?; + doc = Document::from(resp.as_str()); + } + } + } + + let iframe = match doc.select(Attr("name", "view")).next() { + Some(view) => view, + None => { + fs::write("./dump_login_err.html", resp.as_str()).unwrap(); + panic!("failed to get view iframe"); + } + }; + let iframe_src = iframe.attr("src").unwrap(); + + let session_captures = SESSION_RGX.captures(iframe_src).unwrap(); + let session = session_captures.get(1).unwrap().as_str(); + + self.session = session.to_owned(); + Ok(()) + } + + fn logout(&mut self) -> Result<()> { + let full_url = format!("{}/{}", &self.config.url, &self.config.page_php); + let params = [ + ("action", "logout"), + ("session", &self.session), + ("lang", LANG), + ]; + self.client.post(&full_url).form(¶ms).send()?; + self.session = "".to_owned(); + Ok(()) + } + + fn start_cycle(&self, color_only: bool) { + let username = self.base_client.username.clone(); + let tx = self.tx.clone(); + let color_rx = Arc::clone(&self.color_rx); + thread::spawn(move || { + let mut idx = 0; + let colors = vec![ + "#ff3366", "#ff6633", "#FFCC33", "#33FF66", "#33FFCC", "#33CCFF", "#3366FF", + "#6633FF", "#CC33FF", "#efefef", + ]; + loop { + let color_rx = color_rx.lock().unwrap(); + let timeout = after(time::Duration::from_millis(5200)); + select! { + recv(&color_rx) -> _ => break, + recv(&timeout) -> _ => {} + } + idx = (idx + 1) % colors.len(); + let color = colors[idx].to_owned(); + if !color_only { + let name = format!("{}{}", username, random_string(14)); + eprintln!("New name : {}", name); + tx.send(PostType::Profile(color, name)).unwrap(); + } else { + tx.send(PostType::NewColor(color)).unwrap(); + } + // tx.send(PostType::Post("!up".to_owned(), Some(username.clone()))) + // .unwrap(); + // tx.send(PostType::DeleteLast).unwrap(); + } + let msg = PostType::Profile("#90ee90".to_owned(), username); + tx.send(msg).unwrap(); + }); + } + + fn handle_input( + &mut self, + events: &Events, + app: &mut App, + messages: &Arc>>, + users: &Arc>, + ) -> std::result::Result<(), ExitSignal> { + match events.next() { + Ok(Event::NeedLogin) => return Err(ExitSignal::NeedLogin), + Ok(Event::Terminate) => return Err(ExitSignal::Terminate), + Ok(Event::Input(evt)) => self.handle_event(app, messages, users, evt), + _ => Ok(()), + } + } + + fn handle_event( + &mut self, + app: &mut App, + messages: &Arc>>, + users: &Arc>, + event: crossterm::event::Event, + ) -> std::result::Result<(), ExitSignal> { + match event { + crossterm::event::Event::Resize(_cols, _rows) => Ok(()), + crossterm::event::Event::Key(key_event) => { + self.handle_key_event(app, messages, users, key_event) + } + crossterm::event::Event::Mouse(mouse_event) => { + self.handle_mouse_event(app, mouse_event) + } + } + } + + fn handle_key_event( + &mut self, + app: &mut App, + messages: &Arc>>, + users: &Arc>, + key_event: crossterm::event::KeyEvent, + ) -> std::result::Result<(), ExitSignal> { + match app.input_mode { + InputMode::LongMessage => { + self.handle_long_message_mode_key_event(app, key_event, messages) + } + InputMode::Normal => self.handle_normal_mode_key_event(app, key_event, messages), + InputMode::Editing => self.handle_editing_mode_key_event(app, key_event, users), + } + } + + fn handle_long_message_mode_key_event( + &mut self, + app: &mut App, + key_event: crossterm::event::KeyEvent, + messages: &Arc>>, + ) -> std::result::Result<(), ExitSignal> { + match key_event { + KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + } + | KeyEvent { + code: KeyCode::Esc, + modifiers: KeyModifiers::NONE, + } => { + app.long_message = None; + app.input_mode = InputMode::Normal; + } + KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::CONTROL, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(item) = app.items.items.get(idx) { + self.post_msg(PostType::Clean(item.date.to_owned(), item.text.text())) + .unwrap(); + let mut messages = messages.lock().unwrap(); + if let Some(pos) = messages + .iter() + .position(|m| m.date == item.date && m.text.text() == item.text.text()) + { + messages[pos].hide = !messages[pos].hide; + } + app.long_message = None; + app.input_mode = InputMode::Normal; + } + } + } + _ => {} + } + Ok(()) + } + + fn handle_normal_mode_key_event( + &mut self, + app: &mut App, + key_event: crossterm::event::KeyEvent, + messages: &Arc>>, + ) -> std::result::Result<(), ExitSignal> { + match key_event { + KeyEvent { + code: KeyCode::Char('/'), + modifiers: KeyModifiers::NONE, + } => { + app.items.unselect(); + app.input = "/".to_owned(); + app.input_idx = app.input.width(); + app.input_mode = InputMode::Editing; + } + KeyEvent { + code: KeyCode::Char('j'), + modifiers: KeyModifiers::NONE, + } + | KeyEvent { + code: KeyCode::Down, + modifiers: KeyModifiers::NONE, + } => { + app.items.next(); + } + KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::NONE, + } + | KeyEvent { + code: KeyCode::Up, + modifiers: KeyModifiers::NONE, + } => { + app.items.previous(); + } + KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(item) = app.items.items.get(idx) { + app.long_message = Some(item.clone()); + app.input_mode = InputMode::LongMessage; + } + } + } + KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(item) = app.items.items.get(idx) { + let mut messages = messages.lock().unwrap(); + if let Some(pos) = messages + .iter() + .position(|m| m.date == item.date && m.text.text() == item.text.text()) + { + if item.deleted { + messages.remove(pos); + } else { + messages[pos].hide = !messages[pos].hide; + } + } + } + } + } + KeyEvent { + code: KeyCode::Char('y'), + modifiers: KeyModifiers::NONE, + } + | KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(item) = app.items.items.get(idx) { + if let Some(upload_link) = &item.upload_link { + let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); + let mut out = format!("{}{}", self.config.url, upload_link); + if let Some((_, _, msg)) = + get_message(&item.text, &self.config.members_tag) + { + out = format!("{} {}", msg, out); + } + ctx.set_contents(out).unwrap(); + } else if let Some((_, _, msg)) = + get_message(&item.text, &self.config.members_tag) + { + let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); + ctx.set_contents(msg).unwrap(); + } + } + } + } + KeyEvent { + code: KeyCode::Char('Y'), + modifiers: KeyModifiers::SHIFT, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(item) = app.items.items.get(idx) { + if let Some(upload_link) = &item.upload_link { + let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); + let out = format!("{}{}", self.config.url, upload_link); + ctx.set_contents(out).unwrap(); + } else if let Some((_, _, msg)) = + get_message(&item.text, &self.config.members_tag) + { + let finder = LinkFinder::new(); + let links: Vec<_> = finder.links(msg.as_str()).collect(); + if let Some(link) = links.get(0) { + let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); + ctx.set_contents(link.as_str().to_owned()).unwrap(); + } + } + } + } + } + KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::NONE, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(item) = app.items.items.get(idx) { + eprintln!("{:?}", item.text.text()); + } + } + } + KeyEvent { + code: KeyCode::Char('D'), + modifiers: KeyModifiers::SHIFT, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(item) = app.items.items.get(idx) { + eprintln!("{:?} {:?}", item.text, item.upload_link); + } + } + } + KeyEvent { + code: KeyCode::Char('m'), + modifiers: KeyModifiers::NONE, + } => { + let mut is_muted = self.is_muted.lock().unwrap(); + *is_muted = !*is_muted; + } + KeyEvent { + code: KeyCode::Char('M'), + modifiers: KeyModifiers::SHIFT, + } => { + self.show_sys = !self.show_sys; + } + KeyEvent { + code: KeyCode::Char('G'), + modifiers: KeyModifiers::SHIFT, + } => { + self.display_guest_view = !self.display_guest_view; + } + KeyEvent { + code: KeyCode::Char('H'), + modifiers: KeyModifiers::SHIFT, + } => { + self.display_hidden_msgs = !self.display_hidden_msgs; + } + KeyEvent { + code: KeyCode::Char('i'), + modifiers: KeyModifiers::NONE, + } => { + app.input_mode = InputMode::Editing; + app.items.unselect(); + } + KeyEvent { + code: KeyCode::Char('Q'), + modifiers: KeyModifiers::SHIFT, + } => { + self.logout().unwrap(); + return Err(ExitSignal::Terminate); + } + KeyEvent { + code: KeyCode::Char('q'), + modifiers: KeyModifiers::NONE, + } => { + return Err(ExitSignal::Terminate); + } + KeyEvent { + code: KeyCode::Char('t'), + modifiers: KeyModifiers::NONE, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(username) = get_username( + &self.base_client.username, + &app.items.items.get(idx).unwrap().text, + &self.config.members_tag, + ) { + app.input = format!("@{} ", username); + app.input_idx = app.input.width(); + app.input_mode = InputMode::Editing; + app.items.unselect(); + } + } + } + KeyEvent { + code: KeyCode::Char('p'), + modifiers: KeyModifiers::NONE, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(username) = get_username( + &self.base_client.username, + &app.items.items.get(idx).unwrap().text, + &self.config.members_tag, + ) { + app.input = format!("/pm {} ", username); + app.input_idx = app.input.width(); + app.input_mode = InputMode::Editing; + app.items.unselect(); + } + } + } + KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::CONTROL, + } => { + if let Some(idx) = app.items.state.selected() { + if let Some(username) = get_username( + &self.base_client.username, + &app.items.items.get(idx).unwrap().text, + &self.config.members_tag, + ) { + app.input = format!("/kick {} ", username); + app.input_idx = app.input.width(); + app.input_mode = InputMode::Editing; + app.items.unselect(); + } + } + } + KeyEvent { + code: KeyCode::Char('u'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { + code: KeyCode::PageUp, + modifiers: KeyModifiers::NONE, + } => { + if let Some(idx) = app.items.state.selected() { + app.items.state.select(idx.checked_sub(10).or(Some(0))); + } else { + app.items.next(); + } + } + KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { + code: KeyCode::PageDown, + modifiers: KeyModifiers::NONE, + } => { + if let Some(idx) = app.items.state.selected() { + let wanted_idx = idx + 10; + let max_idx = app.items.items.len() - 1; + let new_idx = std::cmp::min(wanted_idx, max_idx); + app.items.state.select(Some(new_idx)); + } else { + app.items.next(); + } + } + KeyEvent { + code: KeyCode::Esc, + modifiers: KeyModifiers::NONE, + } => { + app.items.unselect(); + } + KeyEvent { + code: KeyCode::Char('u'), + modifiers: KeyModifiers::SHIFT, + } => { + app.items.state.select(Some(0)); + } + _ => {} + } + Ok(()) + } + + fn handle_editing_mode_key_event( + &mut self, + app: &mut App, + key_event: crossterm::event::KeyEvent, + users: &Arc>, + ) -> std::result::Result<(), ExitSignal> { + match key_event { + KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + } => { + if FIND_RGX.is_match(&app.input) { + return Ok(()); + } + + let input: String = app.input.drain(..).collect(); + app.input_idx = 0; + if input == "/dl" { + // Delete last message + self.post_msg(PostType::DeleteLast).unwrap(); + } else if let Some(captures) = DLX_RGX.captures(&input) { + // Delete the last X messages + let x: usize = captures.get(1).unwrap().as_str().parse().unwrap(); + for _ in 0..x { + self.post_msg(PostType::DeleteLast).unwrap(); + } + } else if input == "/dall" { + // Delete all messages + self.post_msg(PostType::DeleteAll).unwrap(); + } else if input == "/cycles" { + self.color_tx.send(()).unwrap(); + } else if input == "/cycle1" { + self.start_cycle(true); + } else if input == "/cycle2" { + self.start_cycle(false); + } else if input == "/kall" { + // Kick all guests + let username = "s _".to_owned(); + let msg = "".to_owned(); + self.post_msg(PostType::Kick(msg, username)).unwrap(); + } else if input.starts_with("/m ") { + // Send message to "members" section + let msg = remove_prefix(&input, "/m ").to_owned(); + let to = Some(SEND_TO_MEMBERS.to_owned()); + self.post_msg(PostType::Post(msg, to)).unwrap(); + app.input = "/m ".to_owned(); + app.input_idx = app.input.width() + } else if input.starts_with("/a ") { + // Send message to "admin" section + let msg = remove_prefix(&input, "/a ").to_owned(); + let to = Some(SEND_TO_ADMINS.to_owned()); + self.post_msg(PostType::Post(msg, to)).unwrap(); + app.input = "/a ".to_owned(); + app.input_idx = app.input.width() + } else if input.starts_with("/s ") { + // Send message to "staff" section + let msg = remove_prefix(&input, "/s ").to_owned(); + let to = Some(SEND_TO_STAFFS.to_owned()); + self.post_msg(PostType::Post(msg, to)).unwrap(); + app.input = "/s ".to_owned(); + app.input_idx = app.input.width() + } else if let Some(captures) = PM_RGX.captures(&input) { + // Send PM message + let username = &captures[1]; + let msg = captures[2].to_owned(); + let to = Some(username.to_owned()); + self.post_msg(PostType::Post(msg, to)).unwrap(); + app.input = format!("/pm {} ", username); + app.input_idx = app.input.width() + } else if let Some(captures) = NEW_NICKNAME_RGX.captures(&input) { + // Change nickname + let new_nickname = captures[1].to_owned(); + self.post_msg(PostType::NewNickname(new_nickname)).unwrap(); + } else if let Some(captures) = NEW_COLOR_RGX.captures(&input) { + // Change color + let new_color = captures[1].to_owned(); + self.post_msg(PostType::NewColor(new_color)).unwrap(); + } else if let Some(captures) = KICK_RGX.captures(&input) { + // Kick a user + let username = captures[1].to_owned(); + let msg = captures[2].to_owned(); + self.post_msg(PostType::Kick(msg, username)).unwrap(); + } else if let Some(captures) = IGNORE_RGX.captures(&input) { + // Ignore a user + let username = captures[1].to_owned(); + self.post_msg(PostType::Ignore(username)).unwrap(); + } else if let Some(captures) = UNIGNORE_RGX.captures(&input) { + // Unignore a user + let username = captures[1].to_owned(); + self.post_msg(PostType::Unignore(username)).unwrap(); + } else if let Some(captures) = UPLOAD_RGX.captures(&input) { + // Upload a file + let file_path = captures[1].to_owned(); + let send_to = match captures.get(2) { + Some(to_match) => match to_match.as_str() { + "members" => SEND_TO_MEMBERS, + "staffs" => SEND_TO_STAFFS, + "admins" => SEND_TO_ADMINS, + _ => SEND_TO_ALL, + }, + None => SEND_TO_ALL, + } + .to_owned(); + let msg = match captures.get(3) { + Some(msg_match) => msg_match.as_str().to_owned(), + None => "".to_owned(), + }; + self.post_msg(PostType::Upload(file_path, send_to, msg)) + .unwrap(); + } else { + // Send normal message + self.post_msg(PostType::Post(input, None)).unwrap(); + } + } + KeyEvent { + code: KeyCode::Tab, + modifiers: KeyModifiers::NONE, + } => { + let (p1, p2) = app.input.split_at(app.input_idx); + if p2 == "" || p2.chars().nth(0) == Some(' ') { + let mut parts: Vec<&str> = p1.split(" ").collect(); + if let Some(user_prefix) = parts.pop() { + let mut should_autocomplete = false; + let mut prefix = ""; + if parts.len() == 1 + && ((parts[0] == "/kick" || parts[0] == "/k" || parts[0] == "/pm") + || parts[0] == "/ignore" + || parts[0] == "/unignore") + { + should_autocomplete = true; + } else if user_prefix.starts_with("@") { + should_autocomplete = true; + prefix = "@"; + } + if should_autocomplete { + let user_prefix_norm = remove_prefix(user_prefix, prefix); + let user_prefix_norm_len = user_prefix_norm.len(); + if let Some(name) = autocomplete_username(users, user_prefix_norm) { + let complete_name = format!("{}{}", prefix, name); + parts.push(complete_name.as_str()); + let p2 = p2.trim_start(); + if p2 != "" { + parts.push(p2); + } + app.input = parts.join(" "); + app.input_idx += name.len() - user_prefix_norm_len; + } + } + } + } + } + KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + } => { + app.clear_filter(); + app.input = "".to_owned(); + app.input_idx = 0; + app.input_mode = InputMode::Normal; + } + KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::CONTROL, + } => { + app.input_idx = 0; + } + KeyEvent { + code: KeyCode::Char('e'), + modifiers: KeyModifiers::CONTROL, + } => { + app.input_idx = app.input.width(); + } + KeyEvent { + code: KeyCode::Char('f'), + modifiers: KeyModifiers::CONTROL, + } => { + if let Some(idx) = app.input.chars().skip(app.input_idx).position(|c| c == ' ') { + app.input_idx = std::cmp::min(app.input_idx + idx + 1, app.input.width()); + } else { + app.input_idx = app.input.width(); + } + } + KeyEvent { + code: KeyCode::Char('b'), + modifiers: KeyModifiers::CONTROL, + } => { + if let Some(idx) = app.input_idx.checked_sub(2) { + let tmp = app + .input + .chars() + .take(idx) + .collect::() + .chars() + .rev() + .collect::(); + if let Some(idx) = tmp.chars().position(|c| c == ' ') { + app.input_idx = std::cmp::max(tmp.width() - idx, 0); + } else { + app.input_idx = 0; + } + } + } + KeyEvent { + code: KeyCode::Char('v'), + modifiers: KeyModifiers::CONTROL, + } => { + let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); + if let Ok(clipboard) = ctx.get_contents() { + let byte_position = byte_pos(&app.input, app.input_idx).unwrap(); + app.input.insert_str(byte_position, &clipboard); + app.input_idx += clipboard.width(); + } + } + KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::NONE, + } => { + if app.input_idx > 0 { + app.input_idx -= 1; + } + } + KeyEvent { + code: KeyCode::Right, + modifiers: KeyModifiers::NONE, + } => { + if app.input_idx < app.input.width() { + app.input_idx += 1; + } + } + KeyEvent { + code: KeyCode::Down, + modifiers: KeyModifiers::NONE, + } => { + app.input_mode = InputMode::Normal; + app.items.next(); + } + KeyEvent { + code: KeyCode::Char(c), + modifiers: KeyModifiers::NONE, + } + | KeyEvent { + code: KeyCode::Char(c), + modifiers: KeyModifiers::SHIFT, + } => { + let byte_position = byte_pos(&app.input, app.input_idx).unwrap(); + app.input.insert(byte_position, c); + + app.input_idx += 1; + app.update_filter(); + } + KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, + } => { + if app.input_idx > 0 { + app.input_idx -= 1; + app.input = remove_at(&app.input, app.input_idx); + app.update_filter(); + } + } + KeyEvent { + code: KeyCode::Delete, + modifiers: KeyModifiers::NONE, + } => { + if app.input_idx > 0 && app.input_idx == app.input.width() { + app.input_idx -= 1; + } + app.input = remove_at(&app.input, app.input_idx); + app.update_filter(); + } + KeyEvent { + code: KeyCode::Esc, + modifiers: KeyModifiers::NONE, + } => { + app.input_mode = InputMode::Normal; + } + _ => {} + } + Ok(()) + } + + fn handle_mouse_event( + &mut self, + app: &mut App, + mouse_event: MouseEvent, + ) -> std::result::Result<(), ExitSignal> { + match mouse_event.kind { + MouseEventKind::ScrollDown => app.items.next(), + MouseEventKind::ScrollUp => app.items.previous(), + _ => {} + } + Ok(()) + } +} + +// Give a char index, return the byte position +fn byte_pos(v: &str, idx: usize) -> Option { + let mut b = 0; + let mut chars = v.chars(); + for _ in 0..idx { + if let Some(c) = chars.next() { + b += c.len_utf8(); + } else { + return None; + } + } + Some(b) +} + +// Remove the character at idx (utf-8 aware) +fn remove_at(v: &str, idx: usize) -> String { + v.chars() + .enumerate() + .flat_map(|(i, c)| { + if i == idx { + return None; + } + Some(c) + }) + .collect::() +} + +// Autocomplete any username +fn autocomplete_username(users: &Arc>, prefix: &str) -> Option { + let users = users.lock().unwrap(); + let all_users = users.all(); + let mut filtered = all_users.iter().find(|(_, name)| name.starts_with(prefix)); + if filtered.is_none() { + let prefix_lower = prefix.to_lowercase(); + filtered = all_users + .iter() + .find(|(_, name)| name.to_lowercase().starts_with(&prefix_lower)); + } + match filtered { + Some((_, name)) => Some(name.to_owned()), + None => None, + } +} + +fn set_profile_base_info( + client: &Client, + full_url: &str, + params: &mut Vec<(&str, String)>, +) -> Result<()> { + params.extend(vec![("action", "profile".to_owned())]); + let profile_resp = client.post(full_url).form(¶ms).send()?; + let profile_resp_txt = profile_resp.text().unwrap(); + let doc = Document::from(profile_resp_txt.as_str()); + let bold = doc.select(Attr("id", "bold")).next().unwrap(); + let italic = doc.select(Attr("id", "italic")).next().unwrap(); + let small = doc.select(Attr("id", "small")).next().unwrap(); + if let Some(_) = bold.attr("checked") { + params.push(("bold", "on".to_owned())); + } + if let Some(_) = italic.attr("checked") { + params.push(("italic", "on".to_owned())); + } + if let Some(_) = small.attr("checked") { + params.push(("small", "on".to_owned())); + } + let font_select = doc.select(Attr("name", "font")).next().unwrap(); + let font = font_select.select(Name("option")).find_map(|el| { + if let Some(_) = el.attr("selected") { + return Some(el.attr("value").unwrap()); + } + None + }); + params.push(("font", font.unwrap_or("").to_owned())); + Ok(()) +} + +fn delete_message( + client: &Client, + full_url: &str, + params: &mut Vec<(&str, String)>, + date: String, + text: String, +) -> Result<()> { + params.extend(vec![ + ("action", "admin".to_owned()), + ("do", "clean".to_owned()), + ("what", "choose".to_owned()), + ]); + let clean_resp = client.post(full_url).form(¶ms).send()?; + let clean_resp_txt = clean_resp.text().unwrap(); + let doc = Document::from(clean_resp_txt.as_str()); + let nc = doc.select(Attr("name", "nc")).next().unwrap(); + let nc_value = nc.attr("value").unwrap().to_owned(); + let msgs = extract_messages(&doc).unwrap(); + if let Some(msg) = msgs + .iter() + .find(|m| m.date == date && m.text.text() == text) + { + params.extend(vec![ + ("nc", nc_value.to_owned()), + ("what", "selected".to_owned()), + ("mid[]", format!("{}", msg.id.unwrap())), + ]); + client.post(full_url).form(¶ms).send()?; + } + Ok(()) +} + +fn ban_imposters(tx: &crossbeam_channel::Sender, account_username: &str, users: &Users) { + if BAN_IMPOSTERS { + if users.admin.len() == 0 && (users.staff.len() == 0 || account_username == N0TR1V) { + let n0tr1v_rgx = Regex::new(r#"n[o|0]tr[1|i|l][v|y]"#).unwrap(); // o 0 | 1 i l | v y + let molester_rgx = Regex::new(r#"m[o|0][1|l][e|3][s|5|$]t[e|3]r"#).unwrap(); + let rapist_rgx = Regex::new(r#"r[a|4]p[i|1|l]st"#).unwrap(); + let hitler_rgx = Regex::new(r#"h[i|1|l]t[l|1]er"#).unwrap(); + let himmler_rgx = Regex::new(r#"h[i|1]m+l[e|3]r"#).unwrap(); + let goebbels_rgx = Regex::new(r#"g[o|0][e|3]b+[e|3]ls"#).unwrap(); + let heydrich_rgx = Regex::new(r#"h[e|3]ydr[i|1]ch"#).unwrap(); + let globocnik_rgx = Regex::new(r#"gl[o|0]b[o|0]cn[i|1|l]k"#).unwrap(); + let dirlewanger_rgx = Regex::new(r#"d[i|1]rl[e|3]wang[e|3]r"#).unwrap(); + let jeckeln_rgx = Regex::new(r#"j[e|3]ck[e|3]ln"#).unwrap(); + let kramer_rgx = Regex::new(r#"kram[e|3]r"#).unwrap(); + let blobel_rgx = Regex::new(r#"bl[o|0]b[e|3]l"#).unwrap(); + let stangl_rgx = Regex::new(r#"stangl"#).unwrap(); + for (_color, username) in &users.guests { + let lower_name = username.to_lowercase(); + // Names that anyone using bhcli will ban + if n0tr1v_rgx.is_match(&lower_name) || lower_name.contains("pedo") { + let msg = "forbidden name".to_owned(); + let username = username.to_owned(); + tx.send(PostType::Kick(msg, username)).unwrap(); + } + // Names that only "n0tr1v" will ban + if account_username == N0TR1V { + if lower_name.contains("fuck") + || lower_name.contains("nigger") + || lower_name.contains("nigga") + || lower_name.contains("chink") + || lower_name.contains("atomwaffen") + || lower_name.contains("altright") + || hitler_rgx.is_match(&lower_name) + || goebbels_rgx.is_match(&lower_name) + || himmler_rgx.is_match(&lower_name) + || heydrich_rgx.is_match(&lower_name) + || globocnik_rgx.is_match(&lower_name) + || dirlewanger_rgx.is_match(&lower_name) + || jeckeln_rgx.is_match(&lower_name) + || kramer_rgx.is_match(&lower_name) + || blobel_rgx.is_match(&lower_name) + || stangl_rgx.is_match(&lower_name) + || rapist_rgx.is_match(&lower_name) + || molester_rgx.is_match(&lower_name) + { + let msg = "forbidden name".to_owned(); + let username = username.to_owned(); + tx.send(PostType::Kick(msg, username)).unwrap(); + } + } + } + } + } +} + +struct CustomClient<'a> { + le_chat_php_client: LeChatPHPClient<'a>, +} + +impl ChatClient for CustomClient<'_> { + fn run_forever(&mut self) { + self.le_chat_php_client.run_forever(); + } +} + +impl<'a> CustomClient<'a> { + fn new(params: Params<'a>) -> Self { + let mut c = new_default_le_chat_php_client(params.clone()); + c.config.url = params.url.unwrap_or("".to_owned()); + c.config.page_php = params.page_php.unwrap_or("chat.php".to_owned()); + c.config.datetime_fmt = params.datetime_fmt.unwrap_or("%m-%d %H:%M:%S".to_owned()); + c.config.members_tag = params.members_tag.unwrap_or("[M] ".to_owned()); + c.config.keepalive_send_to = None; + Self { + le_chat_php_client: c, + } + } +} + +struct BHClient<'a> { + le_chat_php_client: LeChatPHPClient<'a>, +} + +impl ChatClient for BHClient<'_> { + fn run_forever(&mut self) { + self.le_chat_php_client.run_forever(); + } +} + +fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient { + let (color_tx, color_rx) = crossbeam_channel::unbounded(); + let (tx, rx) = crossbeam_channel::unbounded(); + LeChatPHPClient { + base_client: BaseClient { + username: params.username, + password: params.password, + }, + max_login_retry: params.max_login_retry, + guest_color: params.guest_color, + session: "".to_owned(), + client: params.client, + dkf_api_key: params.dkf_api_key, + manual_captcha: params.manual_captcha, + refresh_rate: params.refresh_rate, + config: LeChatPHPConfig::new_black_hat_chat_config(), + is_muted: Arc::new(Mutex::new(false)), + show_sys: false, + display_guest_view: false, + display_hidden_msgs: false, + tx, + rx: Arc::new(Mutex::new(rx)), + color_tx, + color_rx: Arc::new(Mutex::new(color_rx)), + } +} + +impl<'a> BHClient<'a> { + fn new(params: Params<'a>) -> Self { + let mut c = new_default_le_chat_php_client(params); + c.config = LeChatPHPConfig::new_black_hat_chat_config(); + c.manual_captcha = true; + Self { + le_chat_php_client: c, + } + } +} + +trait ChatClient { + fn run_forever(&mut self); +} + +struct DanClient<'a> { + le_chat_php_client: LeChatPHPClient<'a>, +} + +impl ChatClient for DanClient<'_> { + fn run_forever(&mut self) { + self.le_chat_php_client.run_forever(); + } +} + +impl<'a> DanClient<'a> { + fn new(params: Params<'a>) -> Self { + let mut c = new_default_le_chat_php_client(params); + c.config = LeChatPHPConfig::new_dans_chat_config(); + c.manual_captcha = true; + Self { + le_chat_php_client: c, + } + } +} + +#[derive(Debug, Clone)] +struct Params<'a> { + url: Option, + page_php: Option, + datetime_fmt: Option, + members_tag: Option, + username: String, + password: String, + guest_color: String, + client: &'a Client, + dkf_api_key: Option, + manual_captcha: bool, + refresh_rate: u64, + max_login_retry: isize, +} + +#[derive(Clone)] +enum ExitSignal { + Terminate, + NeedLogin, +} +struct Sig { + tx: crossbeam_channel::Sender, + rx: crossbeam_channel::Receiver, + nb_rx: usize, +} + +impl Sig { + fn new() -> Self { + let (tx, rx) = crossbeam_channel::unbounded(); + let nb_rx = 0; + Self { tx, rx, nb_rx } + } + + fn clone(&mut self) -> crossbeam_channel::Receiver { + self.nb_rx += 1; + self.rx.clone() + } + + fn signal(&self, signal: ExitSignal) { + for _ in 0..self.nb_rx { + self.tx.send(signal.clone()).unwrap(); + } + } +} + +fn trim_newline(s: &mut String) { + if s.ends_with('\n') { + s.pop(); + if s.ends_with('\r') { + s.pop(); + } + } +} + +fn get_guest_color(wanted: Option) -> String { + match wanted.as_deref() { + Some("beige") => "F5F5DC", + Some("blue-violet") => "8A2BE2", + Some("brown") => "A52A2A", + Some("cyan") => "00FFFF", + Some("sky-blue") => "00BFFF", + Some("gold") => "FFD700", + Some("gray") => "808080", + Some("green") => "008000", + Some("hot-pink") => "FF69B4", + Some("light-blue") => "ADD8E6", + Some("light-green") => "90EE90", + Some("lime-green") => "32CD32", + Some("magenta") => "FF00FF", + Some("olive") => "808000", + Some("orange") => "FFA500", + Some("orange-red") => "FF4500", + Some("red") => "FF0000", + Some("royal-blue") => "4169E1", + Some("see-green") => "2E8B57", + Some("sienna") => "A0522D", + Some("silver") => "C0C0C0", + Some("tan") => "D2B48C", + Some("teal") => "008080", + Some("violet") => "EE82EE", + Some("white") => "FFFFFF", + Some("yellow") => "FFFF00", + Some("yellow-green") => "9ACD32", + Some(other) => COLOR1_RGX + .captures(other) + .map_or("", |captures| captures.get(1).map_or("", |m| m.as_str())), + None => "", + } + .to_owned() +} + +fn get_tor_client(socks_proxy_url: &str) -> Client { + // Create client + let mut builder = reqwest::blocking::ClientBuilder::new() + .cookie_store(true) + .user_agent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"); + + if socks_proxy_url != "" { + let proxy = match reqwest::Proxy::all(socks_proxy_url) { + Ok(p) => p, + Err(e) => { + eprintln!("{}", e); + process::exit(1); + } + }; + builder = builder.proxy(proxy); + } + + let client = match builder.build() { + Ok(c) => c, + Err(e) => { + eprintln!("{}", e); + process::exit(1); + } + }; + client +} + +fn ask_username(username: Option) -> String { + match username { + Some(u) => u, + None => { + print!("username: "); + let mut username_input = String::new(); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut username_input).unwrap(); + trim_newline(&mut username_input); + username_input + } + } +} + +fn ask_password(password: Option) -> String { + match password { + Some(p) => p, + None => rpassword::prompt_password_stdout("Password: ").unwrap(), + } +} + +enum ClientType { + BHC, + Dan, + Custom, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DkfNotifierResp { + #[serde(rename = "NewMessageSound")] + pub new_message_sound: bool, + #[serde(rename = "TaggedSound")] + pub tagged_sound: bool, + #[serde(rename = "PmSound")] + pub pm_sound: bool, + #[serde(rename = "InboxCount")] + pub inbox_count: i64, + #[serde(rename = "LastMessageCreatedAt")] + pub last_message_created_at: String, +} + +fn start_dkf_notifier(client: &Client, dkf_api_key: &str) { + let client = client.clone(); + let dkf_api_key = dkf_api_key.to_owned(); + let mut last_known_date = chrono::offset::Utc::now(); + thread::spawn(move || loop { + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); + + let params: Vec<(&str, String)> = vec![( + "last_known_date", + last_known_date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true), + )]; + let right_url = format!("{}/api/v1/chat/1/notifier", DKF_URL); + if let Ok(resp) = client + .post(right_url) + .form(¶ms) + .header("DKF_API_KEY", &dkf_api_key) + .send() + { + if let Ok(txt) = resp.text() { + if let Ok(v) = serde_json::from_str::(&txt) { + if v.pm_sound || v.tagged_sound { + stream_handle.play_raw(source.convert_samples()).unwrap(); + } + last_known_date = DateTime::parse_from_rfc3339(&v.last_message_created_at) + .unwrap() + .with_timezone(&Utc); + } + } + } + thread::sleep(time::Duration::from_secs(5)); + }); +} + +// Start thread that looks for new emails on DNMX every minutes. +fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) { + let params: Vec<(&str, &str)> = vec![("login_username", username), ("secretkey", password)]; + let login_url = format!("{}/src/redirect.php", DNMX_URL); + client.post(login_url).form(¶ms).send().unwrap(); + + let client_clone = client.clone(); + thread::spawn(move || loop { + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); + + let right_url = format!("{}/src/right_main.php", DNMX_URL); + if let Ok(resp) = client_clone.get(right_url).send() { + let mut nb_mails = 0; + let doc = Document::from(resp.text().unwrap().as_str()); + if let Some(table) = doc.select(Name("table")).nth(7) { + table.select(Name("tr")).skip(1).for_each(|n| { + if let Some(td) = n.select(Name("td")).nth(2) { + if let Some(_) = td.select(Name("b")).nth(0) { + nb_mails += 1; + } + } + }); + } + if nb_mails > 0 { + eprintln!("{} new mails", nb_mails); + stream_handle.play_raw(source.convert_samples()).unwrap(); + } + } + thread::sleep(time::Duration::from_secs(60)); + }); +} + +fn main() -> Result<()> { + let mut opts: Opts = Opts::parse(); + + // Configs file + let cfg: MyConfig = confy::load("bhcli")?; + if opts.dkf_api_key.is_none() { + opts.dkf_api_key = cfg.dkf_api_key; + } + if let Some(default_profile) = cfg.profiles.get(&opts.profile) { + if opts.username.is_none() { + opts.username = Some(default_profile.username.clone()); + opts.password = Some(default_profile.password.clone()); + } + } + + let client = get_tor_client(&opts.socks_proxy_url); + + // If dnmx username is set, start mail notifier thread + if let Some(dnmx_username) = opts.dnmx_username { + start_dnmx_mail_notifier(&client, &dnmx_username, &opts.dnmx_password.unwrap()) + } + + if let Some(dkf_api_key) = &opts.dkf_api_key { + start_dkf_notifier(&client, dkf_api_key); + } + + let guest_color = get_guest_color(opts.guest_color); + let username = ask_username(opts.username); + let password = ask_password(opts.password); + let params = Params { + url: opts.url, + page_php: opts.page_php, + datetime_fmt: opts.datetime_fmt, + members_tag: opts.members_tag, + username, + password, + guest_color, + client: &client, + dkf_api_key: opts.dkf_api_key, + manual_captcha: opts.manual_captcha, + refresh_rate: opts.refresh_rate, + max_login_retry: opts.max_login_retry, + }; + + let chat_type = if params.url.is_some() { + ClientType::Custom + } else if opts.dan { + ClientType::Dan + } else { + ClientType::BHC + }; + + let mut chat_client: Box = match chat_type { + ClientType::Custom => Box::new(CustomClient::new(params)), + ClientType::BHC => Box::new(BHClient::new(params)), + ClientType::Dan => Box::new(DanClient::new(params)), + }; + chat_client.run_forever(); + + Ok(()) +} + +#[derive(Debug, Clone)] +enum PostType { + Post(String, Option), // Message, SendTo + Kick(String, String), // Message, Username + Upload(String, String, String), // FilePath, SendTo, Message + DeleteLast, // DeleteLast + DeleteAll, // DeleteAll + NewNickname(String), // NewUsername + NewColor(String), // NewColor + Profile(String, String), // NewColor, NewUsername + Ignore(String), // Username + Unignore(String), // Username + Clean(String, String), // Clean message +} + +// Get username of other user (or ours if it's the only one) +fn get_username(own_username: &str, root: &StyledText, members_tag: &str) -> Option { + match get_message(root, members_tag) { + Some((from, Some(to), _)) => { + if from == own_username { + return Some(to); + } + return Some(from); + } + Some((from, None, _)) => { + return Some(from); + } + _ => return None, + } +} + +// Extract "from"/"to"/"message content" from a "StyledText" +fn get_message(root: &StyledText, members_tag: &str) -> Option<(String, Option, String)> { + if let StyledText::Styled(_, children) = root { + let msg = match children.get(0) { + Some(el) => el.text(), + _ => return None, + }; + if let Some(StyledText::Styled(_, children)) = children.get(children.len() - 1) { + let from = match children.get(children.len() - 1) { + Some(StyledText::Text(t)) => t.to_owned(), + _ => return None, + }; + return Some((from, None, msg)); + } else if let Some(StyledText::Text(t)) = children.get(children.len() - 1) { + if t == &members_tag { + let from = match children.get(children.len() - 2) { + Some(StyledText::Styled(_, children)) => match children.get(children.len() - 1) + { + Some(StyledText::Text(t)) => t.to_owned(), + _ => return None, + }, + _ => return None, + }; + return Some((from, None, msg)); + } else if t == "[" { + let from = match children.get(children.len() - 2) { + Some(StyledText::Styled(_, children)) => match children.get(children.len() - 1) + { + Some(StyledText::Text(t)) => t.to_owned(), + _ => return None, + }, + _ => return None, + }; + let to = match children.get(2) { + Some(StyledText::Styled(_, children)) => match children.get(children.len() - 1) + { + Some(StyledText::Text(t)) => Some(t.to_owned()), + _ => return None, + }, + _ => return None, + }; + return Some((from, to, msg)); + } + } + } + return None; +} + +#[derive(Debug, PartialEq, Clone)] +enum MessageType { + UserMsg, + SysMsg, +} + +#[derive(Debug, PartialEq, Clone)] +struct Message { + id: Option, + typ: MessageType, + date: String, + upload_link: Option, + text: StyledText, + deleted: bool, // Either or not a message was deleted on the chat + hide: bool, // Either ot not to hide a specific message +} + +#[derive(Debug, PartialEq, Clone)] +enum StyledText { + Styled(tuiColor, Vec), + Text(String), + None, +} + +impl StyledText { + fn walk(&self, mut clb: F) + where + F: FnMut(StyledText), + { + let mut v: Vec<&StyledText> = vec![self]; + loop { + if let Some(e) = v.pop() { + clb(e.clone()); + if let StyledText::Styled(_, children) = e { + v.extend(children); + } + continue; + } + break; + } + } + + fn text(&self) -> String { + let mut s = String::new(); + self.walk(|n| { + if let StyledText::Text(t) = n { + s += &t; + } + }); + s + } + + // Return a vector of each text parts & what color it should be + fn colored_text(&self) -> Vec<(tuiColor, String)> { + let mut out: Vec<(tuiColor, String)> = vec![]; + let mut v: Vec<(tuiColor, &StyledText)> = vec![(tuiColor::White, self)]; + loop { + if let Some((el_color, e)) = v.pop() { + match e { + StyledText::Styled(tui_color, children) => { + for child in children { + v.push((*tui_color, child)); + } + } + StyledText::Text(t) => { + out.push((el_color, t.to_owned())); + } + StyledText::None => {} + } + continue; + } + break; + } + out + } +} + +fn parse_color(color_str: &str) -> tuiColor { + let mut color = tuiColor::White; + if color_str == "red" { + return tuiColor::Red; + } + if let Ok(rgb) = Rgb::from_hex_str(color_str) { + color = tuiColor::Rgb( + rgb.get_red() as u8, + rgb.get_green() as u8, + rgb.get_blue() as u8, + ); + } + color +} + +fn process_node(e: select::node::Node, mut color: tuiColor) -> (StyledText, Option) { + match e.data() { + select::node::Data::Element(_, _) => { + let mut upload_link: Option = None; + if e.name() == Some("span") { + if let Some(style) = e.attr("style") { + if let Some(captures) = COLOR_RGX.captures(style) { + let color_match = captures.get(1).unwrap().as_str(); + color = parse_color(color_match); + } + } + } else if e.name() == Some("font") { + if let Some(color_str) = e.attr("color") { + color = parse_color(color_str); + } + } else if e.name() == Some("a") { + color = tuiColor::White; + if let Some(class) = e.attr("class") { + if class == "attachement" { + if let Some(ahref) = e.attr("href") { + upload_link = Some(ahref.to_owned()); + } + } + } + } + let mut children_texts: Vec = vec![]; + let children = e.children(); + for child in children { + let (st, ul) = process_node(child, color); + if let Some(_) = &ul { + upload_link = ul; + } + children_texts.push(st); + } + children_texts.reverse(); + (StyledText::Styled(color, children_texts), upload_link) + } + select::node::Data::Text(t) => (StyledText::Text(t.to_string()), None), + select::node::Data::Comment(_) => (StyledText::None, None), + } +} + +struct Users { + admin: Vec<(tuiColor, String)>, + staff: Vec<(tuiColor, String)>, + members: Vec<(tuiColor, String)>, + guests: Vec<(tuiColor, String)>, +} + +impl Default for Users { + fn default() -> Self { + Self { + admin: Default::default(), + staff: Default::default(), + members: Default::default(), + guests: Default::default(), + } + } +} + +impl Users { + fn all(&self) -> Vec<&(tuiColor, String)> { + let mut out = Vec::new(); + out.extend(&self.admin); + out.extend(&self.staff); + out.extend(&self.members); + out.extend(&self.guests); + out + } +} + +fn extract_users(doc: &Document) -> Users { + let mut admin = Vec::new(); + let mut staff = Vec::new(); + let mut members = Vec::new(); + let mut guests = Vec::new(); + + if let Some(chatters) = doc.select(Attr("id", "chatters")).next() { + if let Some(tr) = chatters.select(Name("tr")).next() { + let mut th_count = 0; + for e in tr.children() { + if let select::node::Data::Element(_, _) = e.data() { + if e.name() == Some("th") { + th_count += 1; + continue; + } + for user_span in e.select(Name("span")) { + if let Some(user_style) = user_span.attr("style") { + if let Some(captures) = COLOR_RGX.captures(user_style) { + if let Some(color_match) = captures.get(1) { + let color = color_match.as_str().to_owned(); + let tui_color = parse_color(&color); + let username = user_span.text(); + match th_count { + 1 => admin.push((tui_color, username)), + 2 => staff.push((tui_color, username)), + 3 => members.push((tui_color, username)), + 4 => guests.push((tui_color, username)), + _ => {} + } + } + } + } + } + } + } + } + } + Users { + admin, + staff, + members, + guests, + } +} + +fn remove_suffix<'a>(s: &'a str, suffix: &str) -> &'a str { + match s.strip_suffix(suffix) { + Some(s) => s, + None => s, + } +} + +fn remove_prefix<'a>(s: &'a str, prefix: &str) -> &'a str { + match s.strip_prefix(prefix) { + Some(s) => s, + None => s, + } +} + +fn extract_messages(doc: &Document) -> Result> { + let msgs = doc + .select(Attr("id", "messages")) + .next() + .ok_or("failed to get messages div")? + .select(Attr("class", "msg")) + .filter_map(|tag| { + let mut id: Option = None; + if let Some(checkbox) = tag.select(Name("input")).next() { + let id_value: usize = checkbox.attr("value").unwrap().parse().unwrap(); + id = Some(id_value); + } + if let Some(date_node) = tag.select(Name("small")).next() { + if let Some(msg_span) = tag.select(Name("span")).next() { + let date = remove_suffix(&date_node.text(), " - ").to_owned(); + let typ = match msg_span.attr("class") { + Some("usermsg") => MessageType::UserMsg, + Some("sysmsg") => MessageType::SysMsg, + _ => return None, + }; + let (text, upload_link) = process_node(msg_span, tuiColor::White); + return Some(Message { + id, + typ, + date, + upload_link, + text, + deleted: false, + hide: false, + }); + } + } + None + }) + .collect::>(); + Ok(msgs) +} + +fn draw_terminal_frame( + f: &mut Frame>, + app: &mut App, + messages: &Arc>>, + users: &Arc>, +) { + if app.long_message.is_none() { + let hchunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(1), Constraint::Length(25)].as_ref()) + .split(f.size()); + + { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(hchunks[0]); + + render_help_txt(f, app, chunks[0]); + render_textbox(f, app, chunks[1]); + render_messages(f, app, chunks[2], messages); + render_users(f, hchunks[1], users); + } + } else { + let hchunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(1)]) + .split(f.size()); + { + render_long_message(f, app, hchunks[0]); + } + } +} + +fn gen_lines(msg_txt: &StyledText, w: usize, line_prefix: String) -> Vec> { + let txt = msg_txt.text(); + let wrapped = textwrap::fill(&txt, w); + let splits = wrapped.split("\n").collect::>(); + let mut new_lines: Vec> = Vec::new(); + let mut ctxt = msg_txt.colored_text(); + ctxt.reverse(); + let mut ptr = 0; + let mut split_idx = 0; + let mut line: Vec<(tuiColor, String)> = Vec::new(); + let mut first_in_line = true; + loop { + if let Some((color, mut txt)) = ctxt.pop() { + txt = txt.replace("\n", ""); + if let Some(split) = splits.get(split_idx) { + if let Some(chr) = txt.chars().next() { + if chr == ' ' && first_in_line { + let skipped: String = txt.chars().skip(1).collect(); + txt = skipped; + } + } + + let txt = txt.as_str(); + + let remain = split.len() - ptr; + if txt.len() <= remain { + ptr += txt.len(); + line.push((color, txt.to_owned())); + first_in_line = false; + } else { + line.push((color, txt[0..remain].to_owned())); + new_lines.push(line.clone()); + line.clear(); + line.push((tuiColor::White, line_prefix.clone())); + ctxt.push((color, txt[(remain)..].to_owned())); + ptr = 0; + split_idx += 1; + first_in_line = true; + } + } + } else { + new_lines.push(line.clone()); + break; + } + } + new_lines +} + +fn render_long_message(f: &mut Frame>, app: &mut App, r: Rect) { + if let Some(m) = &app.long_message { + let new_lines = gen_lines(&m.text, (r.width - 2) as usize, "".to_owned()); + + let mut rows = vec![]; + let mut spans_vec = vec![]; + for line in new_lines.into_iter() { + for (color, txt) in line { + spans_vec.push(Span::styled(txt, Style::default().fg(color))); + } + rows.push(Spans::from(spans_vec.clone())); + spans_vec.clear(); + } + + let messages_list_items = vec![ListItem::new(rows)]; + + let messages_list = List::new(messages_list_items) + .block(Block::default().borders(Borders::ALL).title("")) + .highlight_style( + Style::default() + .bg(tuiColor::Rgb(50, 50, 50)) + .add_modifier(Modifier::BOLD), + ); + + f.render_widget(messages_list, r); + } +} + +fn render_help_txt(f: &mut Frame>, app: &mut App, r: Rect) { + let (mut msg, style) = match app.input_mode { + InputMode::Normal => ( + vec![ + Span::raw("Press "), + Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("i", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to start editing."), + ], + Style::default(), + ), + InputMode::Editing => ( + vec![ + Span::raw("Press "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to stop editing, "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to record the message"), + ], + Style::default(), + ), + InputMode::LongMessage => (vec![], Style::default()), + }; + if app.is_muted { + let fg = tuiColor::Red; + let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); + msg.extend(vec![Span::raw(" | "), Span::styled("muted", style)]); + } else { + let fg = tuiColor::LightGreen; + let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); + msg.extend(vec![Span::raw(" | "), Span::styled("not muted", style)]); + } + if app.display_guest_view { + let fg = tuiColor::LightGreen; + let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); + msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]); + } else { + let fg = tuiColor::Gray; + let style = Style::default().fg(fg); + msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]); + } + if app.display_hidden_msgs { + let fg = tuiColor::LightGreen; + let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); + msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]); + } else { + let fg = tuiColor::Gray; + let style = Style::default().fg(fg); + msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]); + } + let mut text = Text::from(Spans::from(msg)); + text.patch_style(style); + let help_message = Paragraph::new(text); + f.render_widget(help_message, r); +} + +fn render_textbox(f: &mut Frame>, app: &mut App, r: Rect) { + let w = (r.width - 3) as usize; + let str = app.input.clone(); + let mut input_str = str.as_str(); + let mut overflow = 0; + if app.input_idx >= w { + overflow = std::cmp::max(app.input.width() - w, 0); + input_str = &str[overflow..]; + } + let input = Paragraph::new(input_str) + .style(match app.input_mode { + InputMode::LongMessage => Style::default(), + InputMode::Normal => Style::default(), + InputMode::Editing => Style::default().fg(tuiColor::Yellow), + }) + .block(Block::default().borders(Borders::ALL).title("Input")); + f.render_widget(input, r); + match app.input_mode { + InputMode::LongMessage => {} + InputMode::Normal => + // Hide the cursor. `Frame` does this by default, so we don't need to do anything here + {} + + InputMode::Editing => { + // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering + f.set_cursor( + // Put cursor past the end of the input text + r.x + app.input_idx as u16 - overflow as u16 + 1, + // Move one line down, from the border to the input line + r.y + 1, + ) + } + } +} + +fn render_messages( + f: &mut Frame>, + app: &mut App, + r: Rect, + messages: &Arc>>, +) { + // Messages + app.items.items.clear(); + let messages = messages.lock().unwrap(); + let messages_list_items: Vec = messages + .iter() + .filter_map(|m| { + if !app.display_hidden_msgs && m.hide { + return None; + } + // Simulate a guest view (remove "PMs" and "Members chat" messages) + if app.display_guest_view { + // TODO: this is not efficient at all + if m.text.text().starts_with(&app.members_tag) + || m.text.text().starts_with(&app.staffs_tag) + { + return None; + } + if let Some((_, Some(_), _)) = get_message(&m.text, &app.members_tag) { + return None; + } + } + + if app.filter != "" { + if !m + .text + .text() + .to_lowercase() + .contains(&app.filter.to_lowercase()) + { + return None; + } + } + + app.items.items.push(m.clone()); + + let new_lines = gen_lines(&m.text, (r.width - 20) as usize, " ".repeat(17)); + + let mut rows = vec![]; + let date_style = match (m.deleted, m.hide) { + (false, true) => Style::default().fg(tuiColor::Gray), + (false, _) => Style::default().fg(tuiColor::DarkGray), + (true, _) => Style::default().fg(tuiColor::Red), + }; + let mut spans_vec = vec![Span::styled(m.date.clone(), date_style)]; + let show_sys_sep = app.show_sys && m.typ == MessageType::SysMsg; + let sep = if show_sys_sep { " * " } else { " - " }; + spans_vec.push(Span::raw(sep)); + for (idx, line) in new_lines.into_iter().enumerate() { + // Spams can take your whole screen, so we limit to 5 lines. + if idx >= 5 { + spans_vec.push(Span::styled( + " […]", + Style::default().fg(tuiColor::White), + )); + rows.push(Spans::from(spans_vec.clone())); + break; + } + for (color, txt) in line { + spans_vec.push(Span::styled(txt, Style::default().fg(color))); + } + rows.push(Spans::from(spans_vec.clone())); + spans_vec.clear(); + } + + let mut list_item = ListItem::new(rows); + if m.deleted { + list_item = list_item.style(Style::default().bg(tuiColor::Rgb(30, 0, 0))); + } else if m.hide { + list_item = list_item.style(Style::default().bg(tuiColor::Rgb(20, 20, 20))); + } + + Some(list_item) + }) + .collect(); + + let messages_list = List::new(messages_list_items) + .block(Block::default().borders(Borders::ALL).title("Messages")) + .highlight_style( + Style::default() + .bg(tuiColor::Rgb(50, 50, 50)) + .add_modifier(Modifier::BOLD), + ); + f.render_stateful_widget(messages_list, r, &mut app.items.state) +} + +fn render_users(f: &mut Frame>, r: Rect, users: &Arc>) { + // Users lists + let users = users.lock().unwrap(); + let mut users_list: Vec = vec![]; + let mut users_types: Vec<&Vec<(tuiColor, String)>> = Vec::new(); + users_types.push(&users.admin); + users_types.push(&users.staff); + users_types.push(&users.members); + users_types.push(&users.guests); + for (i, users_type) in users_types.iter().enumerate() { + match i { + 0 => users_list.push(ListItem::new(Span::raw("-- Admin --"))), + 1 => users_list.push(ListItem::new(Span::raw("-- Staff --"))), + 2 => users_list.push(ListItem::new(Span::raw("-- Members --"))), + 3 => users_list.push(ListItem::new(Span::raw("-- Guests --"))), + _ => {} + } + for (tui_color, username) in users_type.iter() { + let span = Span::styled(username, Style::default().fg(*tui_color)); + users_list.push(ListItem::new(span)); + } + } + let users = List::new(users_list).block(Block::default().borders(Borders::ALL).title("Users")); + f.render_widget(users, r); +} + +fn random_string(n: usize) -> String { + let s: Vec = thread_rng().sample_iter(&Alphanumeric).take(n).collect(); + std::str::from_utf8(&s).unwrap().to_owned() +} + +enum InputMode { + LongMessage, + Normal, + Editing, +} + +/// App holds the state of the application +struct App { + /// Current value of the input box + input: String, + input_idx: usize, + /// Current input mode + input_mode: InputMode, + is_muted: bool, + show_sys: bool, + display_guest_view: bool, + display_hidden_msgs: bool, + items: StatefulList, + filter: String, + members_tag: String, + staffs_tag: String, + long_message: Option, +} + +impl Default for App { + fn default() -> App { + App { + input: String::new(), + input_idx: 0, + input_mode: InputMode::Normal, + is_muted: false, + show_sys: false, + display_guest_view: false, + display_hidden_msgs: false, + items: StatefulList::new(), + filter: "".to_owned(), + members_tag: "".to_owned(), + staffs_tag: "".to_owned(), + long_message: None, + } + } +} + +impl App { + fn update_filter(&mut self) { + if let Some(captures) = FIND_RGX.captures(&self.input) { + // Find + self.filter = captures.get(1).map_or("", |m| m.as_str()).to_owned(); + } + } + + fn clear_filter(&mut self) { + if FIND_RGX.is_match(&self.input) { + self.filter = "".to_owned(); + self.input = "".to_owned(); + self.input_idx = 0; + } + } +} + +pub enum Event { + Input(I), + Tick, + Terminate, + NeedLogin, +} + +/// A small event handler that wrap termion input and tick events. Each event +/// type is handled in its own thread and returned to a common `Receiver` +struct Events { + messages_updated_rx: crossbeam_channel::Receiver, + exit_rx: crossbeam_channel::Receiver, + rx: crossbeam_channel::Receiver>, +} + +#[derive(Debug, Clone)] +struct Config { + pub exit_rx: crossbeam_channel::Receiver, + pub messages_updated_rx: crossbeam_channel::Receiver, + pub tick_rate: Duration, +} + +impl Events { + fn with_config(config: Config) -> (Events, thread::JoinHandle<()>) { + let (tx, rx) = crossbeam_channel::unbounded(); + let tick_rate = config.tick_rate; + let exit_rx = config.exit_rx; + let messages_updated_rx = config.messages_updated_rx; + let exit_rx1 = exit_rx.clone(); + let h = thread::spawn(move || { + let mut last_tick = Instant::now(); + loop { + // poll for tick rate duration, if no events, sent tick event. + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if event::poll(timeout).unwrap() { + let evt = event::read().unwrap(); + match evt { + CEvent::Resize(_, _) => tx.send(Event::Input(evt)).unwrap(), + CEvent::Key(_) => tx.send(Event::Input(evt)).unwrap(), + CEvent::Mouse(mouse_event) => { + match mouse_event.kind { + event::MouseEventKind::ScrollDown + | event::MouseEventKind::ScrollUp + | event::MouseEventKind::Down(_) => { + tx.send(Event::Input(evt)).unwrap(); + } + _ => {} + }; + } + }; + } + if last_tick.elapsed() >= tick_rate { + select! { + recv(&exit_rx1) -> _ => break, + default => {}, + } + last_tick = Instant::now(); + } + } + }); + ( + Events { + rx, + exit_rx, + messages_updated_rx, + }, + h, + ) + } + + fn next(&self) -> std::result::Result, crossbeam_channel::RecvError> { + select! { + recv(&self.rx) -> evt => evt, + recv(&self.messages_updated_rx) -> _ => Ok(Event::Tick), + recv(&self.exit_rx) -> v => match v { + Ok(ExitSignal::Terminate) => Ok(Event::Terminate), + Ok(ExitSignal::NeedLogin) => Ok(Event::NeedLogin), + Err(_) => Ok(Event::Terminate), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gen_lines_test() { + let txt = StyledText::Styled( + tuiColor::White, + vec![ + StyledText::Styled( + tuiColor::Rgb(255, 255, 255), + vec![ + StyledText::Text(" prmdbba pwuv💓".to_owned()), + StyledText::Styled( + tuiColor::Rgb(255, 255, 255), + vec![StyledText::Styled( + tuiColor::Rgb(0, 255, 0), + vec![StyledText::Text("PMW".to_owned())], + )], + ), + StyledText::Styled( + tuiColor::Rgb(255, 255, 255), + vec![StyledText::Styled( + tuiColor::Rgb(255, 255, 255), + vec![StyledText::Text("A".to_owned())], + )], + ), + StyledText::Styled( + tuiColor::Rgb(255, 255, 255), + vec![StyledText::Styled( + tuiColor::Rgb(0, 255, 0), + vec![StyledText::Text("XOS".to_owned())], + )], + ), + StyledText::Text( + "pqb a mavx pkj fhsoeycg oruzb asd lk ruyaq re lheot mbnrw ".to_owned(), + ), + ], + ), + StyledText::Text(" - ".to_owned()), + StyledText::Styled( + tuiColor::Rgb(255, 255, 255), + vec![StyledText::Text("rytxvgs".to_owned())], + ), + ], + ); + let lines = gen_lines(&txt, 71, "".to_owned()); + assert_eq!(lines.len(), 2); + } +} diff --git a/src/sound1.mp3 b/src/sound1.mp3 new file mode 100644 index 0000000..7751f7b Binary files /dev/null and b/src/sound1.mp3 differ diff --git a/src/util/event.rs b/src/util/event.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/util/event.rs @@ -0,0 +1 @@ + diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..e90a276 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,55 @@ +pub mod event; + +use tui::widgets::ListState; + +pub struct StatefulList { + pub state: ListState, + pub items: Vec, +} + +impl StatefulList { + pub fn new() -> StatefulList { + StatefulList { + state: ListState::default(), + items: Vec::new(), + } + } + + pub fn next(&mut self) { + if self.items.len() == 0 { + return; + } + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn previous(&mut self) { + if self.items.len() == 0 { + return; + } + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn unselect(&mut self) { + self.state.select(None); + } +}