so-0.4.10/.cargo_vcs_info.json0000644000000001360000000000100115370ustar { "git": { "sha1": "45c073c53b12e54c9f0a18765857deb2ca704291" }, "path_in_vcs": "" }so-0.4.10/CHANGELOG.md000064400000000000000000000060751046102023000121500ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.4.10] #### Fixed - Cloudflare blocking `reqwest`s ([#46](https://github.com/samtay/so/issues/46)) #### Added - Some limited logging - Mac ports install option ([#31](https://github.com/samtay/so/pulls/31)) ## [0.4.9] #### Added - Allow `o` keybinding from the lucky prompt. - Permanent help text ([#24](https://github.com/samtay/so/issues/24)). #### Fixed - Hardcoded stackoverflow link ## [0.4.8] #### Added - Keybinding: Press `o` to open the current answer in the default browser ## [0.4.7] #### Added - Keybinding: Press `y` to yank to system clipboard - New config field for specifying command to copy to system clipboard #### Changed - Switch linux/mac builds from Travis to GitHub Actions. ## [0.4.6] #### Changed - Use Google as the default search engine, due to issues with DuckDuckGo. ## [0.4.5] #### Added - NetBSD installation option. Thanks **voidpin**. #### Fixed - Google parser went out of date - Panic from termimad ([#5](https://github.com/samtay/so/issues/5)) ## [0.4.3] #### Fixed - Build issue caused by syn dependency ([#13](https://github.com/samtay/so/issues/13)) - Panic from termimad ([#8](https://github.com/samtay/so/issues/8)) #### Added - Windows installation option [lukesampson/scoop-extras#4376](https://github.com/lukesampson/scoop-extras/pull/4376). Thanks [@laralex](https://github.com/laralex)! ## [0.4.1] #### Added - GitHub Action to bump homebrew-core formula #### Changed - Homebrew installation method: core [formula](https://formulae.brew.sh/formula/so) now available ## [0.4.0] #### Added - _Keybinding_: Press `q` to quit ([#1](https://github.com/samtay/so/pull/1)). Thanks [@zynaxsoft](https://github.com/zynaxsoft)! - Arch Linux installation options: [so](https://aur.archlinux.org/packages/so/) and [so-git](https://aur.archlinux.org/packages/so-git/) - Homebrew installation option: [samtay/tui/so](https://github.com/samtay/homebrew-tui) - This changelog #### Changed - Default `highlight_text` is now `black` ## [0.3.6] #### Added - CLI spinner #### Fixed - Fragmented highlighting styles ## [0.3.5] - (Unofficial) initial passable release [Unreleased]: (https://github.com/samtay/so/compare/v0.4.9...HEAD) [0.4.9]: (https://github.com/samtay/so/compare/v0.4.8...v0.4.9) [0.4.8]: (https://github.com/samtay/so/compare/v0.4.7...v0.4.8) [0.4.7]: (https://github.com/samtay/so/compare/v0.4.6...v0.4.7) [0.4.6]: (https://github.com/samtay/so/compare/v0.4.5...v0.4.6) [0.4.5]: (https://github.com/samtay/so/compare/v0.4.3...v0.4.5) [0.4.3]: (https://github.com/samtay/so/compare/v0.4.1...v0.4.3) [0.4.1]: (https://github.com/samtay/so/compare/v0.4.0...v0.4.1) [0.4.0]: (https://github.com/samtay/so/compare/v0.3.6...v0.4.0) [0.3.6]: (https://github.com/samtay/so/compare/v0.3.5...v0.3.6) [0.3.5]: (https://github.com/samtay/so/compare/030cd70...v0.3.5) so-0.4.10/Cargo.lock0000644000002442000000000000100075140ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "async-compression" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" dependencies = [ "flate2", "futures-core", "memchr", "pin-project-lite", "tokio", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide 0.7.4", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ "objc2", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "castaway" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ "rustversion", ] [[package]] name = "cc" version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" dependencies = [ "shlex", ] [[package]] name = "cesu8" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags 1.3.2", "textwrap", "unicode-width", ] [[package]] name = "clap" version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] [[package]] name = "compact_str" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" dependencies = [ "castaway", "cfg-if", "itoa", "rustversion", "ryu", "static_assertions", ] [[package]] name = "coolor" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "691defa50318376447a73ced869862baecfab35f6aabaa91a4cd726b315bfe1a" dependencies = [ "crossterm", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "criterion" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" dependencies = [ "atty", "cast", "clap 2.34.0", "criterion-plot", "csv", "itertools", "lazy_static", "num-traits", "oorandom", "plotters", "rayon", "regex", "serde", "serde_cbor", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" dependencies = [ "cast", "itertools", ] [[package]] name = "crokey" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520e83558f4c008ac06fa6a86e5c1d4357be6f994cce7434463ebcdaadf47bb1" dependencies = [ "crokey-proc_macros", "crossterm", "once_cell", "serde", "strict", ] [[package]] name = "crokey-proc_macros" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "370956e708a1ce65fe4ac5bb7185791e0ece7485087f17736d54a23a0895049f" dependencies = [ "crossterm", "proc-macro2", "quote", "strict", "syn 1.0.109", ] [[package]] name = "crossbeam" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", "crossbeam-utils", ] [[package]] name = "crossbeam-channel" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.6.0", "crossterm_winapi", "futures-core", "mio", "parking_lot", "rustix", "signal-hook", "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "cssparser" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" dependencies = [ "cssparser-macros", "dtoa-short", "itoa", "phf 0.11.2", "smallvec", ] [[package]] name = "cssparser-macros" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", "syn 2.0.75", ] [[package]] name = "csv" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" dependencies = [ "csv-core", "itoa", "ryu", "serde", ] [[package]] name = "csv-core" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" dependencies = [ "memchr", ] [[package]] name = "cursive" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "386d5a36020bb856e9a34ecb8a4e6c9bd6b0262d1857bae4db7bc7e2fdaa532e" dependencies = [ "ahash", "cfg-if", "crossbeam-channel", "crossterm", "cursive_core", "lazy_static", "libc", "log", "maplit", "ncurses 6.0.1", "pancurses", "signal-hook", "termion", "unicode-segmentation", "unicode-width", ] [[package]] name = "cursive-macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac7ac0eb0cede3dfdfebf4d5f22354e05a730b79c25fd03481fc69fcfba0a73e" dependencies = [ "proc-macro2", ] [[package]] name = "cursive_core" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "321ec774d27fafc66e812034d0025f8858bd7d9095304ff8fc200e0b9f9cc257" dependencies = [ "ahash", "compact_str", "crossbeam-channel", "cursive-macros", "enum-map", "enumset", "lazy_static", "log", "num", "parking_lot", "serde_json", "time", "toml", "unicode-segmentation", "unicode-width", "xi-unicode", ] [[package]] name = "darling" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "darling_macro" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", "syn 2.0.75", ] [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "derive_more" version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "directories" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "dtoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dtoa-short" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ "dtoa", ] [[package]] name = "ego-tree" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encoding_rs" version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] [[package]] name = "enum-map" version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" dependencies = [ "enum-map-derive", ] [[package]] name = "enum-map-derive" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "enumset" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" dependencies = [ "darling", "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "env_filter" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", "env_filter", "humantime", "log", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "flate2" version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666" dependencies = [ "crc32fast", "miniz_oxide 0.8.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.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ "mac", "new_debug_unreachable", ] [[package]] name = "futures" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "fxhash" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" dependencies = [ "byteorder", ] [[package]] name = "getopts" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "h2" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "half" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "home" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "html5ever" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" dependencies = [ "log", "mac", "markup5ever", "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "http" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", "socket2", "tokio", "tower", "tower-service", "tracing", ] [[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.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jni" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", "cfg-if", "combine", "jni-sys", "log", "thiserror", "walkdir", "windows-sys 0.45.0", ] [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy-regex" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "576c8060ecfdf2e56995cf3274b4f2d71fa5e4fa3607c1c0b63c10180ee58741" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9efb9e65d4503df81c615dc33ff07042a9408ac7f26b45abee25566f7fbfd12c" dependencies = [ "proc-macro2", "quote", "regex", "syn 2.0.75", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libredox" version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" dependencies = [ "bitflags 2.6.0", "libc", "redox_syscall 0.4.1", ] [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", ] [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ "log", "phf 0.11.2", "phf_codegen 0.11.2", "string_cache", "string_cache_codegen", "tendril", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimad" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" dependencies = [ "once_cell", ] [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "miniz_oxide" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", "log", "wasi", "windows-sys 0.52.0", ] [[package]] name = "native-tls" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "ncurses" version = "5.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e2c5d34d72657dc4b638a1c25d40aae81e4f1c699062f72f467237920752032" dependencies = [ "cc", "libc", "pkg-config", ] [[package]] name = "ncurses" version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd71afa95710e841d173521e8483e764004eb332bdf47bd01d00f568688027f" dependencies = [ "cc", "libc", "pkg-config", ] [[package]] name = "ndk-context" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "numtoa" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" [[package]] name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", "objc2-encode", ] [[package]] name = "objc2-encode" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" [[package]] name = "objc2-foundation" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.6.0", "block2", "libc", "objc2", ] [[package]] name = "object" version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "openssl" version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "pancurses" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0352975c36cbacb9ee99bfb709b9db818bed43af57751797f8633649759d13db" dependencies = [ "libc", "log", "ncurses 5.101.0", "pdcurses-sys", "winreg", ] [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.3", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "pdcurses-sys" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b" dependencies = [ "cc", "libc", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ "phf_shared 0.10.0", ] [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared 0.11.2", ] [[package]] name = "phf_codegen" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", ] [[package]] name = "phf_codegen" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator 0.11.2", "phf_shared 0.11.2", ] [[package]] name = "phf_generator" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ "phf_shared 0.10.0", "rand", ] [[package]] name = "phf_generator" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared 0.11.2", "rand", ] [[package]] name = "phf_macros" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator 0.11.2", "phf_shared 0.11.2", "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "phf_shared" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ "siphasher", ] [[package]] name = "phf_shared" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[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.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "pulldown-cmark" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ "bitflags 2.6.0", "memchr", "unicase", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_termios" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox 0.1.3", "thiserror", ] [[package]] name = "regex" version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "async-compression", "base64", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "windows-registry", ] [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pemfile" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[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.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scraper" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b90460b31bfe1fc07be8262e42c665ad97118d4585869de9345a84d501a9eaf0" dependencies = [ "ahash", "cssparser", "ego-tree", "getopts", "html5ever", "once_cell", "selectors", "tendril", ] [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "selectors" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" dependencies = [ "bitflags 2.6.0", "cssparser", "derive_more", "fxhash", "log", "new_debug_unreachable", "phf 0.10.1", "phf_codegen 0.10.0", "precomputed-hash", "servo_arc", "smallvec", ] [[package]] name = "serde" version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] [[package]] name = "serde_cbor" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" dependencies = [ "half", "serde", ] [[package]] name = "serde_derive" version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "serde_json" version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "servo_arc" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" dependencies = [ "stable_deref_trait", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-mio" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "so" version = "0.4.10" dependencies = [ "anyhow", "clap 4.5.16", "criterion", "crossterm", "cursive", "directories", "env_logger", "futures", "lazy_static", "log", "minimad", "percent-encoding", "pulldown-cmark", "rayon", "reqwest", "scraper", "serde", "serde_json", "serde_yaml", "termimad", "thiserror", "tokio", "webbrowser", ] [[package]] name = "socket2" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strict" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" [[package]] name = "string_cache" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", "parking_lot", "phf_shared 0.10.0", "precomputed-hash", "serde", ] [[package]] name = "string_cache_codegen" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", "proc-macro2", "quote", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" dependencies = [ "futures-core", ] [[package]] name = "system-configuration" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" dependencies = [ "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "tempfile" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "tendril" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" dependencies = [ "futf", "mac", "utf-8", ] [[package]] name = "termimad" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "920e7c4671e79f3d9df269da9c8edf0dbc580044fd727d3594f7bfba5eb6107a" dependencies = [ "coolor", "crokey", "crossbeam", "lazy-regex", "minimad", "serde", "thiserror", "unicode-width", ] [[package]] name = "termion" version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ccce68e518d1173e80876edd54760b60b792750d0cab6444a79101c6ea03848" dependencies = [ "libc", "libredox 0.0.2", "numtoa", "redox_termios", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ "unicode-width", ] [[package]] name = "thiserror" version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ "rustls", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-util" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "toml" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "tower" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", "pin-project", "pin-project-lite", "tokio", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicase" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.75", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webbrowser" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "425ba64c1e13b1c6e8c5d2541c8fac10022ca584f33da781db01b5756aef1f4e" dependencies = [ "block2", "core-foundation", "home", "jni", "log", "ndk-context", "objc2", "objc2-foundation", "url", "web-sys", ] [[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.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-registry" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ "windows-result", "windows-strings", "windows-targets 0.52.6", ] [[package]] name = "windows-result" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ "windows-result", "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] [[package]] name = "winreg" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" dependencies = [ "winapi", ] [[package]] name = "xi-unicode" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn 2.0.75", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" so-0.4.10/Cargo.toml0000644000000053210000000000100075360ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "so" version = "0.4.10" authors = ["Sam Tay "] build = false include = [ "src/**/*", "themes/*", "LICENSE", "README.md", "CHANGELOG.md", ] autobins = false autoexamples = false autotests = false autobenches = false description = "A terminal interface for StackOverflow" homepage = "https://github.com/samtay/so" readme = "README.md" keywords = [ "cli", "tui", "stackoverflow", "stackexchange", ] categories = ["command-line-utilities"] license = "MIT" repository = "https://github.com/samtay/so" [lib] name = "so" path = "src/lib.rs" [[bin]] name = "so" path = "src/main.rs" [dependencies.anyhow] version = "1.0" [dependencies.clap] version = "4.5.16" features = [ "cargo", "string", ] [dependencies.crossterm] version = "0.28.1" features = ["event-stream"] [dependencies.cursive] version = "0.21.1" features = ["toml"] default-features = false [dependencies.directories] version = "5.0.1" [dependencies.env_logger] version = "0.11.5" [dependencies.futures] version = "0.3" [dependencies.lazy_static] version = "1.4" [dependencies.log] version = "0.4.22" [dependencies.minimad] version = "0.13.1" [dependencies.percent-encoding] version = "2.1" [dependencies.pulldown-cmark] version = "0.9.6" default-features = false [dependencies.rayon] version = "1.5" [dependencies.reqwest] version = "0.12.7" features = [ "gzip", "json", ] [dependencies.scraper] version = "0.20.0" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1.0" [dependencies.serde_yaml] version = "0.9" [dependencies.termimad] version = "0.30.0" [dependencies.thiserror] version = "1.0" [dependencies.tokio] version = "1.20" features = ["full"] [dependencies.webbrowser] version = "1.0.1" [dev-dependencies.criterion] version = "0.3" [features] crossterm-backend = ["cursive/crossterm-backend"] default = ["cursive/termion-backend"] ncurses-backend = ["cursive/ncurses-backend"] pancurses-backend = ["cursive/pancurses-backend"] termion-backend = ["cursive/termion-backend"] windows = ["cursive/crossterm-backend"] [badges.appveyor] branch = "master" repository = "samtay/so" service = "github" [badges.travis-ci] branch = "master" repository = "samtay/so" so-0.4.10/Cargo.toml.orig000064400000000000000000000034421046102023000132210ustar 00000000000000[package] name = "so" version = "0.4.10" license = "MIT" description = "A terminal interface for StackOverflow" readme = "README.md" homepage = "https://github.com/samtay/so" repository = "https://github.com/samtay/so" keywords = ["cli", "tui", "stackoverflow", "stackexchange"] categories = ["command-line-utilities"] authors = ["Sam Tay "] edition = "2018" include = ["src/**/*", "themes/*", "LICENSE", "README.md", "CHANGELOG.md"] [badges] appveyor = { repository = "samtay/so", branch = "master", service = "github" } travis-ci = { repository = "samtay/so", branch = "master" } [dev-dependencies] criterion = "0.3" [[bench]] name = "html_parsing" path = "benches/html_parsing.rs" harness = false [[bench]] name = "md_parsing" path = "benches/md_parsing.rs" harness = false [dependencies] anyhow = "1.0" clap = { version = "4.5.16", features = ["cargo", "string"] } crossterm = { version = "0.28.1", features = ["event-stream"] } directories = "5.0.1" env_logger = "0.11.5" futures = "0.3" lazy_static = "1.4" log = "0.4.22" minimad = "0.13.1" percent-encoding = "2.1" pulldown-cmark = { version = "0.9.6", default-features = false } rayon = "1.5" reqwest = { version = "0.12.7", features = ["gzip", "json"] } scraper = "0.20.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" termimad = "0.30.0" thiserror = "1.0" tokio = { version = "1.20", features = ["full"] } webbrowser = "1.0.1" [dependencies.cursive] version = "0.21.1" default-features = false features = ["toml"] [features] default = ["cursive/termion-backend"] windows = ["cursive/crossterm-backend"] termion-backend = ["cursive/termion-backend"] ncurses-backend = ["cursive/ncurses-backend"] pancurses-backend = ["cursive/pancurses-backend"] crossterm-backend = ["cursive/crossterm-backend"] so-0.4.10/LICENSE000064400000000000000000000020501046102023000113310ustar 00000000000000MIT License Copyright (c) 2020 Sam Tay Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. so-0.4.10/README.md000064400000000000000000000206771046102023000116220ustar 00000000000000

[![ci][s0]][l0] [![appveyor][s1]][l1] [![crates][s2]][l2] [![MIT][s3]][l3] [![Packaging status](https://repology.org/badge/tiny-repos/so.svg)](https://repology.org/project/so/versions)

[s0]: https://github.com/samtay/so/actions/workflows/ci.yml/badge.svg [l0]: https://github.com/samtay/so/actions/workflows/ci.yml [s1]: https://ci.appveyor.com/api/projects/status/pu7e63f2sqq6x1iq/branch/master?svg=true [l1]: https://ci.appveyor.com/project/samtay/so/branch/master [s2]: https://img.shields.io/crates/v/so.svg [l2]: https://crates.io/crates/so [s3]: https://img.shields.io/badge/license-MIT-blue.svg [l3]: ./LICENSE
A terminal interface for StackOverflow written in Rust

![](assets/demo.gif)

## example usage While I like the acronym *so*, this tool would actually be better described as *se*: an interface to the StackExchange network. In particular one thing that differentiates it from [similar](https://github.com/santinic/how2) [tools](https://github.com/gleitz/howdoi) is that you can simultaneously search any number of sites in the StackExchange network: ```shell # search using your default configuration $ so how do i reverse a list in python # search for a latex solution $ so --site tex how to put tilde over character # use google to search stackoverflow.com, askubuntu.com, and unix.stackexchange.com $ so -e google -s askubuntu -s stackoverflow -s unix how do i install linux ``` ## installation #### Arch Linux You can install the AUR package [so](https://aur.archlinux.org/packages/so/) (tracks latest release) or [so-git](https://aur.archlinux.org/packages/so-git/) (tracks master), e.g. ``` yay -S so-git ``` #### FreeBSD You can install the package [so](https://cgit.freebsd.org/ports/tree/www/so) via ``` pkg install so ``` #### NetBSD You can install the package [so](https://pkgsrc.se/www/so) via ``` pkgin install so ``` #### MacOS You can install the homebrew [formula](https://formulae.brew.sh/formula/so) ``` brew install so ``` Alternatively, you can use [MacPorts](https://www.macports.org) to install [so](https://ports.macports.org/port/so/): ``` sudo port install so ``` #### Windows If you have [scoop](https://scoop.sh/) you can install via the extras bucket: ```shell # add extras bucket scoop bucket add extras # install so scoop install so ``` #### from source For any OS you can install the crate [so](https://crates.io/crates/so) directly: ``` # everything but windows cargo install so # windows cargo install so --no-default-features --features windows ``` For more information on the feature flags, see [selecting a backend](#selecting-a-backend). #### release binaries Static binaries are available on the [releases page](https://github.com/samtay/so/releases) for common Linux, MacOS, and Windows targets. You can quickly install the one you need to directory `DEST` with: ```bash curl --proto '=https' --tlsv1.2 -sSf https://samtay.github.io/so/install.sh \ | bash -s -- --to DEST ``` Right now I'm only building the most common targets, but in theory it should be easy to add more, so if you don't see what you are looking for just open an issue and I can add it. Here's a list of the [supported targets](https://github.com/japaric/trust#supported-targets). If you don't know what you need, you can install [rustc](https://www.rust-lang.org/tools/install) and open an issue with the output of `rustc -Vv | grep host | cut -d' ' -f2`. ## documentation ### configuration The configuration files for e.g. a user `Alice` can be found in the following directories: - Linux: `/home/alice/.config/so` - Windows: `C:\Users\Alice\AppData\Roaming\Sam Tay\so` - MacOS: `/Users/Alice/Library/Preferences/io.Sam-Tay.so` #### defaults The `config.yml` file lets you specify your CLI defaults. So if you dislike the lucky prompt, always search serverfault.com and unix.stackexchange.com, and want the [fastest search engine](#search-engines), you can set your config file like this: ```yaml # config.yml --- api_key: ~ limit: 10 lucky: false sites: - serverfault - unix search_engine: stackexchange ``` Run `so --help` to see your current defaults. #### themes In the same directory you'll find `colors.toml` which is self-documented. The default theme attempts to blend in with your default terminal theme, but you can change it as necessary. In particular, you may want to change the `highlight_text` if the current selection is difficult to read. There are some themes in the [themes](./themes) directory as well. #### system clipboard integration There's a very primitive integration in place to copy the contents of the currently focused question or answer to the system clipboard. This requires some command in your PATH that can accept stdin and pipe to the clipboard. On mac & windows, this will work out of the box with the default set to `pbcopy` & `clip` respectively. On Linux, I've made the assumption that `xclip` is likely the most popular, but if you use something else (e.g. `wl-copy` on wayland), you'll need to set the command directly: ```yaml # config.yml --- copy_cmd: copy --option-to-take-stdin ``` #### api keys If you want to use your own [StackExchange API Key](https://api.stackexchange.com/docs) you can set it via ``` so --set-api-key ``` You can also choose to use no key by editing your configuration to `api_key: ~`. If for some reason my API key is globally throttled, you can hit the StackExchange API with no key up to 300 times per day per IP, which I imagine is fine for most users. ### search engines The available search engines are StackExchange, DuckDuckGo, and Google. StackExchange will always be the fastest to search because it doesn't require an additional request or any HTML parsing; however, it is also very primitive. ~~DuckDuckGo is in second place for speed, as its response HTML is much smaller than Google's. I've found that it performs well for my queries, so it is the default search engine.~~ DuckDuckGo [sometimes blocks requests](https://github.com/samtay/so/issues/16), so it is no longer the default. ### multi-site searching As stated in the [docs](https://api.stackexchange.com/docs/throttle), > If a single IP is making more than 30 requests a second, new requests will be dropped. So, don't go crazy with the multi-site search, since it is all done in parallel. In particular, if you specify more than 30 sites, SE will likely ban you for a short time. ### selecting a backend If you're installing from source, you can choose from a number of available backend rendering engines. Note that the package `default` and `windows` feature flags do not have an ncurses dependency, for the sake of portability. The default backend is [termion](https://github.com/redox-os/termion), a bindless library in pure Rust which seems to work quite well on Linux, MacOS, BSD, and Redox. The windows backend is by default [crossterm](https://github.com/crossterm-rs/crossterm), and while its level of support is awesome, it does comes at a price in performance. On my machine, the app kind of flashes between draws. So if you are on Mac, Linux, or Redox, your best bet is to compile with default features which uses the termion backend. If you are on windows, use crossterm, but know it will be slightly jumpy. If the crossterm folks figure out a fix for allowing ncurses to receive [resize events](https://github.com/crossterm-rs/crossterm/issues/447), and you have [ncurses installed](https://github.com/gyscos/cursive/wiki/Install-ncurses) on your system, then the ncurses and pancurses backends are likely the most performant. Just know that *currently* if you choose this option, and you run the `--lucky` prompt, you won't be able to resize the terminal window while the TUI is open. Available backends: - `termion-backend` - `ncurses-backend` - `pancurses-backend` - `crossterm-backend` E.g. to use `ncurses-backend`: ``` cargo install so --no-default-features --features ncurses-backend ``` See more information about this choice [here](https://github.com/gyscos/cursive/wiki/Backends). ## contributing **Warning**: this was my first time writing Rust and there is very likely some non-idiomatic and straight up ugly code throughout this project, so don't come looking here for a good Rust example! That being said, I would love to improve the codebase. Feel free to check out the [contributing guidelines](.github/CONTRIBUTING.md) and submit any refactoring issues or pull requests. ## credits Credit to my good friend [Charles](http://heyitscharles.com) for logo design. so-0.4.10/src/cli.rs000064400000000000000000000170521046102023000122400ustar 00000000000000use clap::{ builder::{styling::AnsiColor as Ansi, Styles}, value_parser, Arg, ArgAction, ArgMatches, ColorChoice, Command, }; use crate::config::Config; use crate::error::Result; // TODO --add-site (in addition to defaults) // TODO set_api_key should probably just be a bool, since we have config pub struct Opts { pub list_sites: bool, pub print_config_path: bool, pub update_sites: bool, pub set_api_key: Option, pub query: Option, pub config: Config, } /// Get CLI opts and args, with defaults pulled from user configuration pub fn get_opts() -> Result { get_opts_with(Config::new, |a| a.get_matches()) } /// Get CLI opts, starting with defaults produced from `mk_config` and matching args with /// `get_matches`. fn get_opts_with(mk_config: F, get_matches: G) -> Result where F: FnOnce() -> Result, G: for<'a> FnOnce(Command) -> ArgMatches, { let config = mk_config()?; let limit = config.limit.to_string(); let sites = config.sites.join(";"); let engine = config.search_engine.to_string(); let clapp = Command::new("so") .color(ColorChoice::Always) .styles(STYLES) .version(clap::crate_version!()) .author(clap::crate_authors!()) .about(clap::crate_description!()) .arg( Arg::new("list-sites") .long("list-sites") .action(ArgAction::SetTrue) .help("Print available StackExchange sites"), ) .arg( Arg::new("update-sites") .long("update-sites") .action(ArgAction::SetTrue) .help("Update cache of StackExchange sites"), ) .arg( Arg::new("set-api-key") .long("set-api-key") .num_args(1) .value_name("key") .help("Set StackExchange API key"), ) .arg( Arg::new("print-config-path") .long("print-config-path") .help("Print path to config file") .action(ArgAction::SetTrue) .hide(true), ) .arg( Arg::new("site") .long("site") .short('s') .action(ArgAction::Append) .num_args(1) .default_value(&sites) .value_name("site-code") .help("StackExchange site to search"), ) .arg( Arg::new("limit") .long("limit") .short('l') .num_args(1) .default_value(&limit) .value_name("int") .value_parser(value_parser!(u16)) .help("Question limit"), ) .arg( Arg::new("lucky") .long("lucky") .action(ArgAction::SetTrue) .help("Print the top-voted answer of the most relevant question"), ) .arg( Arg::new("no-lucky") .long("no-lucky") .action(ArgAction::SetTrue) .help("Disable lucky") .conflicts_with("lucky") .hide(!config.lucky), ) .arg( Arg::new("query") .num_args(1..) .index(1) .required_unless_present_any([ "list-sites", "update-sites", "set-api-key", "print-config-path", ]), ) .arg( Arg::new("search-engine") .long("search-engine") .short('e') .num_args(1) .default_value(&engine) .value_name("engine") .value_parser(["duckduckgo", "google", "stackexchange"]) .help("Use specified search engine") .next_line_help(true), ); let matches = get_matches(clapp); let lucky = match (matches.get_flag("lucky"), matches.get_flag("no-lucky")) { (true, _) => true, (_, true) => false, _ => config.lucky, }; Ok(Opts { list_sites: matches.get_flag("list-sites"), print_config_path: matches.get_flag("print-config-path"), update_sites: matches.get_flag("update-sites"), set_api_key: matches.get_one("set-api-key").cloned(), query: matches .get_many::("query") .map(|words| words.map(|s| s.as_str()).collect::>().join(" ")), config: Config { // these unwraps are safe via clap default values & validators limit: *matches.get_one("limit").unwrap(), search_engine: serde_yaml::from_str( matches.get_one::("search-engine").unwrap(), )?, sites: matches .get_many::("site") .expect("at least one site is required!") .flat_map(|s| s.split(';')) .map(String::from) .collect(), api_key: matches.get_one("set-api-key").cloned().or(config.api_key), lucky, ..config }, }) } const STYLES: Styles = Styles::styled() .header(Ansi::Red.on_default().bold()) .usage(Ansi::Red.on_default().bold()) .literal(Ansi::Blue.on_default().bold()) .placeholder(Ansi::Green.on_default()); #[cfg(test)] mod tests { use super::*; use crate::config::SearchEngine; fn defaults() -> Config { Config { api_key: Some(String::from("my key")), limit: 64, lucky: false, sites: vec![ String::from("some"), String::from("sites"), String::from("yeah"), ], search_engine: SearchEngine::DuckDuckGo, copy_cmd: Some(String::from("wl-copy")), } } fn mk_config() -> Result { Ok(defaults()) } #[test] fn test_defaults() { let opts = get_opts_with(mk_config, |a| { a.get_matches_from(vec!["so", "how do I exit Vim"]) }); assert_eq!(opts.unwrap().config, defaults()); } #[test] fn test_overrides() { let opts = get_opts_with(mk_config, |a| { a.get_matches_from(vec!["so", "-s", "english", "how do I exit Vim"]) }); assert_eq!( opts.unwrap().config, Config { sites: vec![String::from("english")], ..defaults() } ); let opts = get_opts_with(mk_config, |a| { a.get_matches_from(vec!["so", "-l", "5", "--lucky", "how do I exit Vim"]) }); assert_eq!( opts.unwrap().config, Config { limit: 5, lucky: true, ..defaults() } ); } #[test] fn test_set_api_key() { let opts = get_opts_with(mk_config, |a| { a.get_matches_from(vec!["so", "--set-api-key", "new key"]) }) .unwrap(); // Uses key in new config assert_eq!( opts.config, Config { api_key: Some(String::from("new key")), ..defaults() } ); // Flags it in opts assert_eq!(opts.set_api_key, Some(String::from("new key"))); } #[test] #[should_panic] fn test_conflicts() { get_opts_with(mk_config, |a| { a.try_get_matches_from(vec!["so", "--lucky", "--no-lucky"]) .unwrap() }) .unwrap(); } } so-0.4.10/src/config.rs000064400000000000000000000102021046102023000127240ustar 00000000000000use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::fmt; use std::fs; use std::io::Write; use std::path::PathBuf; use std::process::Command; use std::process::Stdio; use crate::error::{Error, Result}; use crate::utils; #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "lowercase")] #[derive(Default)] pub enum SearchEngine { DuckDuckGo, #[default] Google, StackExchange, } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] #[serde(default)] pub struct Config { pub api_key: Option, pub limit: u16, pub lucky: bool, pub sites: Vec, pub search_engine: SearchEngine, pub copy_cmd: Option, } impl fmt::Display for SearchEngine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match &self { SearchEngine::DuckDuckGo => "duckduckgo", SearchEngine::Google => "google", SearchEngine::StackExchange => "stackexchange", }; write!(f, "{s}") } } // TODO make a friender config file, like the colors.toml below impl Default for Config { fn default() -> Self { Config { api_key: Some(String::from("8o9g7WcfwnwbB*Qp4VsGsw((")), limit: 20, lucky: true, sites: vec![String::from("stackoverflow")], search_engine: SearchEngine::default(), copy_cmd: Some(String::from(if cfg!(target_os = "macos") { "pbcopy" } else if cfg!(target_os = "windows") { "clip" } else if cfg!(target_os = "linux") { "xclip -sel clip" } else { // this default makes no sense but w/e "wl-copy" })), } } } impl Config { /// Get user config (writes default if none found) pub fn new() -> Result { let project = Self::project_dir()?; let dir = project.config_dir(); fs::create_dir_all(dir)?; let filename = Self::config_file_path()?; match utils::open_file(&filename)? { None => { let def = Config::default(); def.write()?; Ok(def) } Some(file) => serde_yaml::from_reader(file) .map_err(|_| Error::MalformedFile(filename.clone())) .and_then(|cfg: Config| { if cfg.sites.is_empty() { Err(Error::MalformedFile(filename)) } else { Ok(cfg) } }), } } // TODO This looks odd when refactoring to associate functions under Config; perhaps this // shouldn't be a CLI opt? Maybe a generic --save-config based on current opts? pub fn set_api_key(key: String) -> Result<()> { let mut cfg = Self::new()?; cfg.api_key = Some(key); cfg.write() } /// Get project directory pub fn project_dir() -> Result { ProjectDirs::from("io", "Sam Tay", "so").ok_or(Error::ProjectDir) } // TODO consider switching to .toml to be consistent with colors.toml pub fn config_file_path() -> Result { Ok(Self::project_dir()?.config_dir().join("config.yml")) } /// Get theme file path; if it doesn't exist yet, create it with defaults. pub fn theme_file_path() -> Result { let name = Self::project_dir()?.config_dir().join("colors.toml"); if !name.as_path().exists() { let mut file = utils::create_file(&name)?; file.write_all(include_bytes!("../themes/default.toml"))?; } Ok(name) } fn write(&self) -> Result<()> { let filename = Self::config_file_path()?; let file = utils::create_file(&filename)?; Ok(serde_yaml::to_writer(file, &self)?) } pub fn get_copy_cmd(&self) -> Option { let copy_cmd_str = self.copy_cmd.as_ref()?; let mut pieces = copy_cmd_str.split_whitespace(); let mut cmd = Command::new(pieces.next()?); cmd.args(pieces).stdin(Stdio::piped()); Some(cmd) } } so-0.4.10/src/error.rs000064400000000000000000000023251046102023000126170ustar 00000000000000use std::path::PathBuf; pub type Result = std::result::Result; // TODO convert/remove this to just use anyhow #[derive(thiserror::Error, Debug)] pub enum Error { #[error("{0}")] Anyhow(#[from] anyhow::Error), #[error("Termimad error: {0}")] Termimad(#[from] termimad::Error), #[error("Crossterm error: {0}")] IO(#[from] std::io::Error), #[error("Reqwest error: {0}")] Reqwest(#[from] reqwest::Error), #[error("SerdeJson error: {0}")] SerdeJson(#[from] serde_json::Error), #[error("SerdeYaml error: {0}")] SerdeYaml(#[from] serde_yaml::Error), #[error("Futures Join error : {0}")] Join(#[from] tokio::task::JoinError), #[error("File `{}` is malformed; try removing it", .0.display())] MalformedFile(PathBuf), #[error("Lacking {0:?} permissions on `{}`", .1.display())] Permissions(PermissionType, PathBuf), #[error("{0}")] StackExchange(String), #[error("{0}")] Scraping(String), #[error("Couldn't find a suitable project directory; is your OS supported?")] ProjectDir, #[error("Sorry, couldn't find any answers to your question")] NoResults, } #[derive(Debug)] pub enum PermissionType { Read, Write, } so-0.4.10/src/lib.rs000064400000000000000000000001551046102023000122330ustar 00000000000000pub mod cli; pub mod config; pub mod error; pub mod stackexchange; pub mod term; pub mod tui; pub mod utils; so-0.4.10/src/main.rs000064400000000000000000000067241046102023000124210ustar 00000000000000mod cli; mod config; mod error; mod stackexchange; mod term; mod tui; mod utils; use std::{fmt::Write, sync::Arc}; use crossterm::event::{KeyCode, KeyEvent}; use tokio::runtime::Runtime; use tokio::task; use config::Config; use error::{Error, Result}; use stackexchange::{LocalStorage, Search}; use term::Term; fn main() -> Result<()> { env_logger::init(); // Tokio runtime Runtime::new()? .block_on(run()) .map(|app| { // Run TUI app.map(tui::App::run); }) .or_else(|e: Error| { // Handle errors term::print_error(&e.to_string()) }) } /// Runs the CLI and, if the user wishes to enter the TUI, returns /// question/answer data async fn run() -> Result> { // Get CLI opts let opts = cli::get_opts()?; let config = opts.config; let sites = &config.sites; let lucky = config.lucky; // Term tools and markdown styles (outside of TUI) let mut term = Term::new(); let ls = LocalStorage::new(opts.update_sites).await?; if let Some(key) = opts.set_api_key { Config::set_api_key(key)?; } if opts.print_config_path { println!("{}", Config::config_file_path()?.display()); } if opts.list_sites { let mut md = String::new(); md.push_str("|:-:|:-:|\n"); md.push_str("|Site Code|Site URL|\n"); md.push_str("|-:|:-|\n"); for s in ls.sites.iter() { writeln!(&mut md, "|{}|{}", s.api_site_parameter, s.site_url).ok(); } md.push_str("|-\n"); term.print(&md); return Ok(None); } if let Some(site) = ls.find_invalid_site(sites).await { term.print_error(&format!("{site} is not a valid StackExchange site.\n\n"))?; term.print_notice( "If you think this is incorrect, try running \ `so --update-sites` to update the cached site listing. \ You can also run `so --list-sites` to list all available sites.", )?; return Ok(None); } if let Some(q) = opts.query { let site_map = Arc::new(ls.get_site_map(&config.sites)); let mut search = Search::new(config.clone(), Arc::clone(&site_map), q); if lucky { // Show top answer let lucky_answer = Term::wrap_spinner(search.search_lucky()).await??; term.print(&lucky_answer.answer.body); term.print("\nPress **[SPACE]** to see more results, **[o]** to open in the browser, or any other key to exit"); // Kick off the rest of the search in the background let app = task::spawn(async move { tui::App::from_search(search).await }); match Term::wait_for_key().await? { KeyEvent { code: KeyCode::Char(' '), .. } => (), KeyEvent { code: KeyCode::Char('o'), .. } => { let url = site_map.answer_url(&lucky_answer.question, lucky_answer.answer.id); webbrowser::open(&url)?; return Ok(None); } _ => return Ok(None), } // Get the rest of the questions return Ok(Some(Term::wrap_spinner(app).await?.unwrap()?)); } else { return Ok(Some( Term::wrap_spinner(tui::App::from_search(search)).await??, )); } } Ok(None) } so-0.4.10/src/stackexchange/api.rs000064400000000000000000000156201046102023000150510ustar 00000000000000use rayon::prelude::*; use reqwest::header; use reqwest::Client; use reqwest::Url; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use crate::error::Result; use crate::tui::markdown; /// StackExchange API v2.2 URL const SE_API_URL: &str = "https://api.stackexchange.com"; const SE_API_VERSION: &str = "2.2"; /// Filter generated to include only the fields needed to populate /// the structs below. Go here to make new filters: /// [create filter](https://api.stackexchange.com/docs/create-filter). const SE_FILTER: &str = ".DND5X2VHHUH8HyJzpjo)5NvdHI3w6auG"; /// Pagesize when fetching all SE sites. Should be good for many years... const SE_SITES_PAGESIZE: u16 = 10000; pub type Id = u32; /// Represents a StackExchange answer with a custom selection of fields from /// the [StackExchange docs](https://api.stackexchange.com/docs/types/answer) #[derive(Clone, Deserialize, Debug)] pub struct Answer { #[serde(rename = "answer_id")] pub id: Id, pub score: i32, #[serde(rename = "body_markdown")] pub body: S, pub is_accepted: bool, } /// Represents a StackExchange question with a custom selection of fields from /// the [StackExchange docs](https://api.stackexchange.com/docs/types/question) #[derive(Clone, Deserialize, Debug)] pub struct Question { #[serde(rename = "question_id")] pub id: Id, pub score: i32, // N.B. empty vector default needed because questions endpoint cannot filter // answers >= 1 #[serde(default = "Vec::new")] pub answers: Vec>, pub title: String, #[serde(rename = "body_markdown")] pub body: S, // This is the only field that doesn't actually come back from SE; we add // this site code to which the question belongs pub site: Option, } /// Internal struct that represents the boilerplate response wrapper from SE API. #[derive(Deserialize, Debug)] struct ResponseWrapper { items: Vec, } #[derive(Deserialize, Serialize, Debug)] pub struct Site { pub api_site_parameter: String, pub site_url: String, } #[derive(Debug, Clone)] pub struct Api { client: Client, api_key: Option, } impl Api { pub fn new(api_key: Option) -> Self { // TODO can lazy_static this above let mut headers = header::HeaderMap::new(); headers.insert( header::ACCEPT, header::HeaderValue::from_static("application/json"), ); headers.insert( header::USER_AGENT, header::HeaderValue::from_static(super::USER_AGENT), ); let client = Client::builder().default_headers(headers).build().unwrap(); Api { client, api_key } } /// Search against the SE site's /questions/{ids} endpoint. /// Filters out questions with no answers. pub async fn questions(&self, site: &str, ids: Vec) -> Result>> { let total = ids.len().to_string(); let endpoint = format!("questions/{ids}", ids = ids.join(";")); let url = stackexchange_url(&endpoint); log::debug!("Fetching questions from: {url}"); let qs_rsp = self .client .get(url) .query(&self.get_default_se_opts()) .query(&[("site", site), ("pagesize", &total)]) .send() .await?; let status_code = qs_rsp.status(); let body = qs_rsp.text().await?; log::debug!("Stack exchange returned status {status_code} and body {body}"); let qs = serde_json::from_str::>>(&body)? .items .into_iter() .filter(|q| !q.answers.is_empty()) .collect(); Ok(Self::preprocess(site, qs)) } /// Search against the SE site's /search/advanced endpoint with a given query. /// Only fetches questions that have at least one answer. pub async fn search_advanced( &self, query: &str, site: &str, limit: u16, ) -> Result>> { let qs = self .client .get(stackexchange_url("search/advanced")) .query(&self.get_default_se_opts()) .query(&[ ("q", query), ("pagesize", &limit.to_string()), ("site", site), ("answers", "1"), ("order", "desc"), ("sort", "relevance"), ]) .send() .await? .json::>>() .await? .items; Ok(Self::preprocess(site, qs)) } pub async fn sites(&self) -> Result> { let sites = self .client .get(stackexchange_url("sites")) .query(&[("pagesize", SE_SITES_PAGESIZE.to_string())]) .send() .await? .json::>() .await? .items; Ok(sites .into_par_iter() .map(|site| { let site_url = site.site_url.trim_start_matches("https://").to_string(); Site { site_url, ..site } }) .collect()) } fn get_default_se_opts(&self) -> HashMap<&str, &str> { let mut params = HashMap::new(); params.insert("filter", SE_FILTER); params.insert("page", "1"); if let Some(key) = &self.api_key { params.insert("key", key); } params } /// Sorts answers by score /// Add the site code to which the question belongs /// Preprocess SE markdown to "cmark" markdown (or something closer to it) /// This markdown preprocess _always_ happens. fn preprocess(site: &str, qs: Vec>) -> Vec> { qs.into_par_iter() .map(|q| { let mut answers = q.answers; answers.par_sort_unstable_by_key(|a| -a.score); let answers = answers .into_par_iter() .map(|a| Answer { body: markdown::preprocess(a.body.clone()), ..a }) .collect(); Question { answers, site: Some(site.to_string()), body: markdown::preprocess(q.body), ..q } }) .collect::>() } } /// Creates stackexchange API url given endpoint // TODO lazy static this url parse fn stackexchange_url(path: &str) -> Url { let mut url = Url::parse(SE_API_URL).unwrap(); url.path_segments_mut() .unwrap() .push(SE_API_VERSION) .extend(path.split('/')); url } #[cfg(test)] mod tests { use super::*; #[test] fn test_stackexchange_url() { assert_eq!( stackexchange_url("some/endpoint").as_str(), "https://api.stackexchange.com/2.2/some/endpoint" ) } } so-0.4.10/src/stackexchange/local_storage.rs000064400000000000000000000076601046102023000171230ustar 00000000000000use std::collections::HashMap; use std::fs; use std::ops::Deref; use std::path::Path; use crate::config::Config; use crate::error::{Error, Result}; use crate::utils; use super::api::{Api, Site}; use super::Question; /// This structure allows interacting with locally cached StackExchange metadata. pub struct LocalStorage { pub sites: Vec, } impl LocalStorage { fn fetch_local_sites(filename: &Path) -> Result>> { if let Some(file) = utils::open_file(filename)? { return serde_json::from_reader(file) .map_err(|_| Error::MalformedFile(filename.to_path_buf())); } Ok(None) } fn store_local_sites(filename: &Path, sites: &[Site]) -> Result<()> { let file = utils::create_file(filename)?; serde_json::to_writer(file, sites)?; Ok(()) } async fn init_sites(filename: &Path, update: bool) -> Result> { if !update { if let Some(sites) = Self::fetch_local_sites(filename)? { return Ok(sites); } } let sites = Api::new(None).sites().await?; Self::store_local_sites(filename, &sites)?; Ok(sites) } pub async fn new(update: bool) -> Result { let project = Config::project_dir()?; let dir = project.cache_dir(); fs::create_dir_all(dir)?; let sites_filename = dir.join("sites.json"); let sites = Self::init_sites(&sites_filename, update).await?; Ok(LocalStorage { sites }) } // TODO is this HM worth it? Probably only will ever have < 10 site codes to search... // maybe store this as Option on self if other methods use it... pub async fn find_invalid_site<'a, 'b>( &'b self, site_codes: &'a [String], ) -> Option<&'a String> { let hm: HashMap<&str, ()> = self .sites .iter() .map(|site| (site.api_site_parameter.as_str(), ())) .collect(); site_codes.iter().find(|s| !hm.contains_key(&s.as_str())) } pub fn get_site_map(&self, site_codes: &[String]) -> SiteMap { let inner = self .sites .iter() .filter_map(move |site| { let _ = site_codes .iter() .find(|&sc| *sc == site.api_site_parameter)?; Some((site.api_site_parameter.to_owned(), site.site_url.to_owned())) }) .collect(); SiteMap { inner } } } /// Just a map of site codes to site URLs, shareable across the app. These are /// only the sites relevant to the configuration / query, not all cached SE /// sites. #[derive(Debug)] pub struct SiteMap { inner: HashMap, } impl Deref for SiteMap { type Target = HashMap; fn deref(&self) -> &Self::Target { &self.inner } } impl SiteMap { /// Get SE answer url. Panics if site was not set on the question, or /// site code not found in the map. pub fn answer_url(&self, question: &Question, answer_id: u32) -> String { // answer link actually doesn't need question id let site_url = self.site_url(question); format!("https://{site_url}/a/{answer_id}") } /// Get SE question url. Panics if site was not set on the question, or /// site code not found in the map. pub fn question_url(&self, question: &Question) -> String { // answer link actually doesn't need question id let question_id = question.id; let site_url = self.site_url(question); format!("https://{site_url}/q/{question_id}") } fn site_url(&self, question: &Question) -> String { self.inner .get( question .site .as_ref() .expect("bug: site not attached to question"), ) .cloned() .expect("bug: lost a site") } } so-0.4.10/src/stackexchange/mod.rs000064400000000000000000000005121046102023000150510ustar 00000000000000mod api; mod local_storage; mod search; // Exposed for benchmarking pub mod scraper; pub use api::{Answer, Id, Question}; pub use local_storage::{LocalStorage, SiteMap}; pub use search::Search; /// Mock user agent const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:11.0) Gecko/20100101 Firefox/11.0"; so-0.4.10/src/stackexchange/scraper.rs000064400000000000000000000263751046102023000157500ustar 00000000000000use percent_encoding::percent_decode_str; use reqwest::Url; use scraper::html::Html; use scraper::selector::Selector; use std::collections::hash_map::Entry; use std::collections::HashMap; use crate::error::{Error, Result}; /// DuckDuckGo URL const DUCKDUCKGO_URL: &str = "https://duckduckgo.com"; const GOOGLE_URL: &str = "https://google.com/search"; // Is question_id unique across all sites? If not, then this edge case is // unaccounted for when sorting. // // If this is ever an issue, it wouldn't be too hard to account for this; just // keep track of site in the `ordering` field and also return site from the // spawned per-site tasks. #[derive(Debug, PartialEq, Eq)] pub struct ScrapedData { /// Mapping of site code to question ids pub question_ids: HashMap>, /// Mapping of question_id to its ordinal place in search results pub ordering: HashMap, } // TODO add this type system limitation to blog post pub trait Scraper { /// Parse data from search results html fn parse(&self, html: &str, sites: &HashMap, limit: u16) -> Result; /// Get the url to search query restricted to sites fn get_url<'a, I>(&self, query: &str, sites: I) -> Url where I: IntoIterator; } pub struct DuckDuckGo; impl Scraper for DuckDuckGo { /// Parse (site, question_id) pairs out of duckduckgo search results html fn parse( &self, html: &str, sites: &HashMap, limit: u16, ) -> Result { let anchors = Selector::parse("a.result__a").unwrap(); parse_with_selector(anchors, html, sites, limit).and_then(|sd| { // DDG seems to never have empty results, so assume this is blocked if sd.question_ids.is_empty() { Err(Error::Scraping(String::from( "DuckDuckGo blocked this request", ))) } else { Ok(sd) } }) } /// Creates duckduckgo search url given sites and query /// See https://duckduckgo.com/params for more info fn get_url<'a, I>(&self, query: &str, sites: I) -> Url where I: IntoIterator, { let q = make_query_arg(query, sites); Url::parse_with_params( DUCKDUCKGO_URL, &[("q", q.as_str()), ("kz", "-1"), ("kh", "-1")], ) .unwrap() } } pub struct Google; impl Scraper for Google { /// Parse SE data out of google search results html fn parse( &self, html: &str, sites: &HashMap, limit: u16, ) -> Result { let anchors = Selector::parse("a").unwrap(); parse_with_selector(anchors, html, sites, limit) } /// Creates duckduckgo search url given sites and query /// See https://duckduckgo.com/params for more info fn get_url<'a, I>(&self, query: &str, sites: I) -> Url where I: IntoIterator, { let q = make_query_arg(query, sites); Url::parse_with_params(GOOGLE_URL, &[("q", q.as_str())]).unwrap() } } fn make_query_arg<'a, I>(query: &str, sites: I) -> String where I: IntoIterator, { let mut q = String::new(); // Restrict to sites q.push('('); q.push_str( sites .into_iter() .map(|site| String::from("site:") + site) .collect::>() .join(" OR ") .as_str(), ); q.push_str(") "); // Search terms q.push_str( query .trim_end_matches('?') .split_whitespace() .collect::>() .join(" ") .as_str(), ); q } fn parse_with_selector( anchors: Selector, html: &str, sites: &HashMap, limit: u16, ) -> Result { let fragment = Html::parse_document(html); let mut question_ids: HashMap> = HashMap::new(); let mut ordering: HashMap = HashMap::new(); let mut count = 0; for anchor in fragment.select(&anchors) { if let Some(url) = anchor .value() .attr("href") .map(|href| percent_decode_str(href).decode_utf8_lossy()) { sites.iter().find_map(|(site_code, site_url)| { let id = question_url_to_id(site_url, &url)?; ordering.insert(id.to_owned(), count); match question_ids.entry(site_code.to_owned()) { Entry::Occupied(mut o) => o.get_mut().push(id), Entry::Vacant(o) => { o.insert(vec![id]); } } count += 1; Some(()) }); } if count >= limit as usize { break; } } Ok(ScrapedData { question_ids, ordering, }) } // TODO use str_prefix once its stable fn question_url_to_id(site_url: &str, input: &str) -> Option { ["/questions/", "/q/"].iter().find_map(|segment| { let fragment = site_url.trim_end_matches('/').to_owned() + segment; let mut ix = input.find(&fragment)?; if ix > 0 && input.chars().nth(ix - 1) == Some('.') { return None; } ix += fragment.len(); let input = &input[ix..]; let id = if let Some(end) = input.find('/') { input[0..end].to_string() } else { input[0..].to_string() }; if id.chars().all(|c| c.is_ascii_digit()) { Some(id) } else { None } }) } // TODO Get blocked google request html // TODO Get google no results html // note: this may only be possible at search.rs level (with non-200 code) #[cfg(test)] mod tests { use super::*; #[test] fn test_duckduckgo_url() { let q = "how do I exit vim?"; let sites = vec![ String::from("stackoverflow.com"), String::from("unix.stackexchange.com"), ]; assert_eq!( DuckDuckGo.get_url(q, &sites).as_str(), String::from( "https://duckduckgo.com/\ ?q=%28site%3Astackoverflow.com+OR+site%3Aunix.stackexchange.com%29\ +how+do+I+exit+vim&kz=-1&kh=-1" ) ) } #[test] fn test_duckduckgo_parser() { let html = include_str!("../../test/duckduckgo/exit-vim.html"); let sites = vec![ ("stackoverflow", "stackoverflow.com"), ("askubuntu", "askubuntu.com"), ] .into_iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect::>(); let expected_scraped_data = ScrapedData { question_ids: vec![ ("stackoverflow", vec!["11828270", "9171356"]), ("askubuntu", vec!["24406"]), ] .into_iter() .map(|(k, v)| { ( k.to_string(), v.into_iter().map(|s| s.to_string()).collect(), ) }) .collect(), ordering: vec![("11828270", 0), ("9171356", 2), ("24406", 1)] .into_iter() .map(|(k, v)| (k.to_string(), v)) .collect(), }; assert_eq!( DuckDuckGo.parse(html, &sites, 3).unwrap(), expected_scraped_data ); } #[test] fn test_google_parser() { let html = include_str!("../../test/google/exit-vim.html"); let sites = vec![ ("stackoverflow", "stackoverflow.com"), ("askubuntu", "askubuntu.com"), ] .into_iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect::>(); let expected_scraped_data = ScrapedData { question_ids: vec![ ("stackoverflow", vec!["11828270", "25919461"]), ("askubuntu", vec!["24406"]), ] .into_iter() .map(|(k, v)| { ( k.to_string(), v.into_iter().map(|s| s.to_string()).collect(), ) }) .collect(), ordering: vec![("11828270", 0), ("25919461", 1), ("24406", 2)] .into_iter() .map(|(k, v)| (k.to_string(), v)) .collect(), }; assert_eq!( Google.parse(html, &sites, 3).unwrap(), expected_scraped_data ); } #[test] fn test_google_q_parser() { let html = include_str!("../../test/google/parsing-q.html"); let mut sites = HashMap::new(); sites.insert( String::from("stackoverflow"), String::from("stackoverflow.com"), ); let expected_scraped_data = ScrapedData { question_ids: vec![( String::from("stackoverflow"), vec![ String::from("3940128"), String::from("4647368"), String::from("12336105"), ], )] .into_iter() .collect(), ordering: vec![ (String::from("3940128"), 0), (String::from("4647368"), 1), (String::from("12336105"), 2), ] .into_iter() .collect(), }; assert_eq!( Google.parse(html, &sites, 3).unwrap(), expected_scraped_data ); } #[test] fn test_duckduckgo_blocker() -> Result<(), String> { let html = include_str!("../../test/duckduckgo/bad-user-agent.html"); let mut sites = HashMap::new(); sites.insert( String::from("stackoverflow"), String::from("stackoverflow.com"), ); match DuckDuckGo.parse(html, &sites, 2) { Err(Error::Scraping(s)) if s == *"DuckDuckGo blocked this request" => Ok(()), _ => Err(String::from("Failed to detect DuckDuckGo blocker")), } } #[test] fn test_question_url_to_id() { // Happy path let site_url = "stackoverflow.com"; let input = "/l/?kh=-1&uddg=https://stackoverflow.com/questions/11828270/how-do-i-exit-the-vim-editor"; assert_eq!(question_url_to_id(site_url, input).unwrap(), "11828270"); // Happy path with variant /q/ let site_url = "stackoverflow.com"; let input = "/l/?kh=-1&uddg=https://stackoverflow.com/q/11828270"; assert_eq!(question_url_to_id(site_url, input).unwrap(), "11828270"); // Base site let site_url = "unix.stackoverflow.com"; let input = "/l/?kh=-1&uddg=https://unix.stackoverflow.com"; assert_eq!(question_url_to_id(site_url, input), None); // Tagged link let site_url = "meta.stackexchange.com"; let input = "/l/?kh=-1&uddg=https://meta.stackexchange.com/questions/tagged/stackexchange-tour"; assert_eq!(question_url_to_id(site_url, input), None); // Different site let site_url = "meta.stackexchange.com"; let input = "/l/?kh=-1&uddg=https://math.meta.stackexchange.com/q/11828270"; assert_eq!(question_url_to_id(site_url, input), None); } } so-0.4.10/src/stackexchange/search.rs000064400000000000000000000155641046102023000155540ustar 00000000000000use futures::stream::StreamExt; use rayon::prelude::*; use reqwest::header; use reqwest::Client; use std::sync::Arc; use crate::config::{Config, SearchEngine}; use crate::error::{Error, Result}; use crate::tui::markdown; use crate::tui::markdown::Markdown; use super::api::{Answer, Api, Question}; use super::local_storage::SiteMap; use super::scraper::{DuckDuckGo, Google, ScrapedData, Scraper}; /// Limit on concurrent requests (gets passed to `buffer_unordered`) const CONCURRENT_REQUESTS_LIMIT: usize = 8; /// This structure provides methods to search queries and get StackExchange /// questions/answers in return. // TODO this really needs a better name... #[derive(Debug, Clone)] pub struct Search { pub api: Api, pub config: Config, pub query: String, pub site_map: Arc, } #[derive(Debug, Clone)] pub struct LuckyAnswer { /// Preprocessed markdown content pub answer: Answer, /// Parent question pub question: Question, } impl Search { pub fn new(config: Config, site_map: Arc, query: String) -> Self { let api = Api::new(config.api_key.clone()); Search { api, config, query, site_map, } } /// Search query and get the top answer body /// /// For StackExchange engine, use only the first configured site, /// since, parodoxically, sites with the worst results will finish /// executing first, because there's less data to retrieve. /// /// Needs mut because it temporarily changes self.config pub async fn search_lucky(&mut self) -> Result { let original_config = self.config.clone(); // Temp set lucky config self.config.limit = 1; if let SearchEngine::StackExchange = self.config.search_engine { self.config.sites.truncate(1); } // Run search with temp config let result = self.search().await; // Reset config self.config = original_config; let question = result?.into_iter().next().ok_or(Error::NoResults)?; let answer = question.answers.first().cloned().ok_or_else(|| { Error::StackExchange(String::from("Received question with no answers")) })?; Ok(LuckyAnswer { answer, question }) } /// Search and parse to Markdown for TUI pub async fn search_md(&self) -> Result>> { Ok(parse_markdown(self.search().await?)) } /// Search using the configured search engine pub async fn search(&self) -> Result>> { match self.config.search_engine { SearchEngine::DuckDuckGo => self.search_by_scraper(DuckDuckGo).await, SearchEngine::Google => self.search_by_scraper(Google).await, SearchEngine::StackExchange => self.parallel_search_advanced().await, } .and_then(|qs| { if qs.is_empty() { Err(Error::NoResults) } else { Ok(qs) } }) } /// Search query at duckduckgo and then fetch the resulting questions from SE. async fn search_by_scraper(&self, scraper: impl Scraper) -> Result>> { let url = scraper.get_url(&self.query, self.site_map.values()); let html = Client::new() .get(url) .header(header::USER_AGENT, super::USER_AGENT) .send() .await? .text() .await?; let data = scraper.parse(&html, self.site_map.as_ref(), self.config.limit)?; log::debug!("Scraped question IDs: {:#?}", &data.question_ids); self.parallel_questions(data).await } /// Parallel requests against the SE question endpoint across all sites in data. // TODO I'm sure there is a way to DRY the following two functions async fn parallel_questions(&self, data: ScrapedData) -> Result>> { let ScrapedData { question_ids, ordering, } = data; futures::stream::iter(question_ids) .map(|(site, ids)| { let api = self.api.clone(); tokio::spawn(async move { api.questions(&site, ids).await }) }) .buffer_unordered(CONCURRENT_REQUESTS_LIMIT) .collect::>() .await .into_iter() .map(|r| r.map_err(Error::from).and_then(|x| x)) .collect::>>>() .map(|v| { let mut qs: Vec> = v.into_iter().flatten().collect(); qs.sort_unstable_by_key(|q| ordering.get(&q.id.to_string()).unwrap()); qs }) } /// Parallel requests against the SE search/advanced endpoint across all configured sites async fn parallel_search_advanced(&self) -> Result>> { futures::stream::iter(self.config.sites.clone()) .map(|site| { let api = self.api.clone(); let limit = self.config.limit; let query = self.query.clone(); tokio::spawn(async move { api.search_advanced(&query, &site, limit).await }) }) .buffer_unordered(CONCURRENT_REQUESTS_LIMIT) .collect::>() .await .into_iter() .map(|r| r.map_err(Error::from).and_then(|x| x)) .collect::>>>() .map(|v| { let mut qs: Vec> = v.into_iter().flatten().collect(); if self.config.sites.len() > 1 { qs.sort_unstable_by_key(|q| -q.score); } qs }) } } /// Parse all markdown fields /// This only happens for content going into the cursive TUI (not lucky prompt) fn parse_markdown(qs: Vec>) -> Vec> { qs.into_par_iter() .map(|q| { let body = markdown::parse(q.body); let answers = q .answers .into_par_iter() .map(|a| { let body = markdown::parse(a.body); Answer { body, id: a.id, score: a.score, is_accepted: a.is_accepted, } }) .collect::>(); Question { body, answers, id: q.id, score: q.score, title: q.title, site: q.site, } }) .collect::>() } // TODO find a query that returns no results so that I can test it and // differentiate it from a blocked request #[cfg(test)] mod tests { #[test] fn test_duckduckgo_response() { // TODO make sure results are either 1) answers 2) failed connection 3) blocked } } so-0.4.10/src/term.rs000064400000000000000000000122051046102023000124330ustar 00000000000000use anyhow::Context; use crossterm::event::{read, Event, KeyEvent}; use crossterm::style::{Color, Print}; use crossterm::terminal::ClearType; use crossterm::{cursor, execute, terminal}; use futures::Future; use std::io::stderr; use termimad::{CompoundStyle, LineStyle, MadSkin}; use tokio::sync::{ oneshot, oneshot::{error::TryRecvError, Receiver, Sender}, }; use tokio::task::JoinHandle; use tokio::time; use crate::error::Result; const LOADING_SPINNER_DELAY: u64 = 40; const LOADING_SPINNER_DOTS: [&str; 56] = [ "⢀⠀", "⡀⠀", "⠄⠀", "⢂⠀", "⡂⠀", "⠅⠀", "⢃⠀", "⡃⠀", "⠍⠀", "⢋⠀", "⡋⠀", "⠍⠁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⢈⠩", "⡀⢙", "⠄⡙", "⢂⠩", "⡂⢘", "⠅⡘", "⢃⠨", "⡃⢐", "⠍⡐", "⢋⠠", "⡋⢀", "⠍⡁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⠈⠩", "⠀⢙", "⠀⡙", "⠀⠩", "⠀⢘", "⠀⡘", "⠀⠨", "⠀⢐", "⠀⡐", "⠀⠠", "⠀⢀", "⠀⡀", ]; pub struct Term { skin: MadSkin, } impl Default for Term { fn default() -> Self { Term::new() } } struct Spinner { tx: Sender<()>, handle: JoinHandle>, } impl Term { pub fn new() -> Self { let skin = MadSkin { inline_code: CompoundStyle::with_fg(Color::Cyan), code_block: LineStyle { compound_style: CompoundStyle::with_fg(Color::Cyan), ..Default::default() }, ..Default::default() }; Term { skin } } /// Print text to stdout pub fn print(&self, text: &str) { self.skin.print_text(text) } /// Print text with error styling to stderr /// Needs mut to temporarily modify styling (e.g. red fg) pub fn print_error(&mut self, text: &str) -> Result<()> { self.print_with_style(Color::Red, "✖ ", text) } /// Print text with notice styling to stderr /// Needs mut to temporarily modify styling (e.g. yellow fg) pub fn print_notice(&mut self, text: &str) -> Result<()> { self.print_with_style(Color::Yellow, "➜ ", text) } fn print_with_style(&mut self, fg: Color, prefix: &str, text: &str) -> Result<()> { let mut styled_text = String::from(prefix); styled_text.push_str(text); // Set fg self.skin.paragraph.set_fg(fg); self.skin .write_text_on(&mut std::io::stderr(), &styled_text)?; // Unset fg self.skin .paragraph .compound_style .object_style .foreground_color = None; Ok(()) } /// Waits for the user to press any key and returns it pub async fn wait_for_key() -> Result { let res = tokio::task::spawn_blocking(move || { terminal::enable_raw_mode().unwrap(); let k = loop { if let Event::Key(k) = read().unwrap() { break k; } }; terminal::disable_raw_mode().unwrap(); k }) .await .context("failed to get key event")?; Ok(res) } /// As it sounds, takes a future and shows a CLI spinner until it's output is ready pub async fn wrap_spinner(future: F) -> Result where F: Future, { // Start spinner let spinner = Spinner::new(); let result = future.await; // Stop spinner spinner.stop().await?; Ok(result) } } impl Spinner { /// Start a CLI spinner on the current cursor line. To stop it, call `stop` on the returned /// `Spinner`. pub fn new() -> Self { let (tx, rx) = oneshot::channel(); let handle = tokio::spawn(Self::spin(rx)); Spinner { tx, handle } } /// Stop the spinner. This requires a bit of cleanup, and so should be `await`ed before doing /// any other i/o. pub async fn stop(self) -> Result<()> { self.tx.send(()).ok(); self.handle.await??; Ok(()) } /// Spin until receiver finds unit async fn spin(mut rx: Receiver<()>) -> Result<()> { let mut dots = LOADING_SPINNER_DOTS.iter().cycle(); terminal::enable_raw_mode()?; execute!( stderr(), cursor::SavePosition, cursor::Hide, terminal::Clear(ClearType::CurrentLine), )?; let mut interval = time::interval(time::Duration::from_millis(LOADING_SPINNER_DELAY)); while let Err(TryRecvError::Empty) = rx.try_recv() { execute!( stderr(), cursor::MoveToColumn(1), terminal::Clear(ClearType::CurrentLine), Print(dots.next().unwrap()) )?; interval.tick().await; } execute!( stderr(), terminal::Clear(ClearType::CurrentLine), cursor::RestorePosition, cursor::Show, )?; terminal::disable_raw_mode()?; Ok(()) } } pub fn print_error(text: &str) -> Result<()> { Term::new().print_error(text) } so-0.4.10/src/tui/app.rs000064400000000000000000000224221046102023000130470ustar 00000000000000use std::collections::HashMap; use std::io; use std::io::Write; use std::sync::Arc; use cursive::event::{Event, Key}; use cursive::theme::{BaseColor, Color, Effect, Style}; use cursive::traits::{Nameable, Scrollable}; use cursive::utils::markup::StyledString; use cursive::utils::span::SpannedString; use cursive::views::{Dialog, LinearLayout, TextView, ViewRef}; use cursive::Cursive; use cursive::XY; use super::markdown; use super::markdown::Markdown; use super::views::{ LayoutView, ListView, MdView, Name, TempView, Vimable, NAME_ANSWER_LIST, NAME_ANSWER_VIEW, NAME_FULL_LAYOUT, NAME_QUESTION_LIST, NAME_QUESTION_VIEW, NAME_TEMP_MSG, }; use crate::config::Config; use crate::error::Result; use crate::stackexchange::{Answer, Id, Question, Search, SiteMap}; pub const NAME_HELP_VIEW: &str = "help_view"; pub struct App { questions: HashMap>, answers: HashMap>, config: Config, site_map: Arc, } impl App { pub async fn from_search(search: Search) -> Result { let qs = search.search_md().await?; let questions: HashMap> = qs.clone().into_iter().map(|q| (q.id, q)).collect(); let answers: HashMap> = qs .into_iter() .flat_map(|q| q.answers.into_iter().map(|a| (a.id, a))) .collect(); Ok(Self { config: search.config, site_map: search.site_map, questions, answers, }) } // TODO a app field that gets auto updated with new selections would be convenient pub fn run(self) -> Result<()> { // The underlying fields of self are just static data that we // borrow from various places and callbacks; wrap in Arc to just have // one allocation that gets referenced from wherever. let arc = Arc::new(self); let mut siv = cursive::default(); siv.load_theme_file(Config::theme_file_path()?).unwrap(); // TODO dont unwrap let question_view = MdView::new(Name::QuestionView); let answer_view = MdView::new(Name::AnswerView); let arc2 = arc.clone(); let question_list_view = ListView::new_with_items( Name::QuestionList, arc.questions .clone() .into_values() .map(|q| (preview_question(&q), q.id)), move |s, qid| arc2.question_selected_callback(s, *qid), ); let arc2 = arc.clone(); let answer_list_view = ListView::new(Name::AnswerList, move |s, aid| { let a = arc2.answers.get(aid).unwrap(); s.call_on_name(NAME_ANSWER_VIEW, |v: &mut MdView| v.set_content(&a.body)); }); let main_layout = LayoutView::new( 1, question_list_view, question_view, answer_list_view, answer_view, ) .add_vim_bindings(); let hint_text = TextView::new("? help \u{00B7} q quit"); siv.add_layer(LinearLayout::vertical().child(main_layout).child(hint_text)); let cb = siv.call_on_name(NAME_QUESTION_LIST, |v: &mut ListView| v.select(0)); if let Some(cb) = cb { cb(&mut siv) } // Help / View keymappings siv.add_global_callback('?', |s| { if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_HELP_VIEW) { s.screen_mut().remove_layer(pos); } else { s.add_layer(help()); } }); // Reload theme siv.add_global_callback(Event::CtrlChar('r'), |s| { s.load_theme_file(Config::theme_file_path().unwrap()) .unwrap() }); // Copy contents to sys clipboard let arc2 = arc.clone(); siv.add_global_callback('y', move |s| { let mut v: ViewRef = s .find_name(NAME_FULL_LAYOUT) .expect("bug: layout view should exist"); let md = v.get_focused_content(); if let Some(mut copy_cmd) = arc2.config.get_copy_cmd() { let res = (|| { let mut child = copy_cmd.spawn().map_err(|e| { if e.kind() == io::ErrorKind::NotFound { io::Error::new( io::ErrorKind::Other, "couldn't exec copy cmd; you may need to configure it manually", ) } else { e } })?; let mut stdin = child.stdin.take().ok_or_else(|| { io::Error::new(io::ErrorKind::Other, "couldn't get stdin of copy cmd") })?; stdin.write_all(md.source().as_bytes())?; Ok("copied to clipboard!".to_string()) })(); temp_feedback_msg(s, res); } }); // Open in browser let arc2 = arc; siv.add_global_callback('o', move |s| { let mut v: ViewRef = s .find_name(NAME_FULL_LAYOUT) .expect("bug: layout view should exist"); if let Some((qid, aid_opt)) = v.get_focused_ids() { let question = arc2.questions.get(&qid).expect("bug: lost a question?!"); let url = aid_opt .map(|aid| arc2.site_map.answer_url(question, aid)) .unwrap_or_else(|| arc2.site_map.question_url(question)); let res = webbrowser::open(&url) .map(|_| "opened stackexchange in the browser!".to_string()); temp_feedback_msg(s, res); } }); // Close any open dialogs siv.add_global_callback(Event::Key(Key::Esc), |s| { if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_HELP_VIEW) { s.screen_mut().remove_layer(pos); } if let Some(pos) = s.screen_mut().find_layer_from_name(NAME_TEMP_MSG) { s.screen_mut().remove_layer(pos); } }); // Run the app siv.run(); Ok(()) } pub fn question_selected_callback(&self, s: &mut Cursive, qid: u32) { let q = self.questions.get(&qid).unwrap(); let body = &q.body; let XY { x, y: _y } = s.screen_size(); // Update question view s.call_on_name(NAME_QUESTION_VIEW, |v: &mut MdView| { v.set_content(body); }) .expect("Panic: setting question view content failed"); // Update answer list view let cb = s .call_on_name(NAME_ANSWER_LIST, |v: &mut ListView| { v.reset_with_all(q.answers.iter().map(|a| (preview_answer(x, a), a.id))) }) .expect("Panic: setting answer list content failed"); cb(s) } } fn preview_question(q: &Question) -> StyledString { let mut preview = pretty_score(q.score); preview.append_plain(&q.title); preview } fn preview_answer(screen_width: usize, a: &Answer) -> StyledString { let md = markdown::preview(screen_width, &a.body); let mut preview = pretty_score(a.score); if a.is_accepted { preview.append_styled( "\u{2713} ", // "✔ " Style::merge(&[ Style::from(Color::Light(BaseColor::Green)), Style::from(Effect::Bold), ]), ); } preview.append(md); preview } fn pretty_score(score: i32) -> StyledString { let color = if score > 0 { Color::Light(BaseColor::Green) } else { Color::Light(BaseColor::Red) }; SpannedString::styled( format!("({score}) "), Style::merge(&[Style::from(color), Style::from(Effect::Bold)]), ) } // This would be a good usecase for brining in termimad tables pub fn help() -> Dialog { let bindings = r###" ## Panes **Tab**: Focus next pane **Space**: Cycle layout (4 Pane, 2 Pane, FullScreen) ## Scroll **h,j,k,l**: ←,↓,↑,→ **Ctrl**: ↑ x 5 **Ctrl**: ↓ x 5 **Ctrl**: ↑ x 10 **Ctrl**: ↓ x 10 **gg**: Scroll To Top **G**: Scroll To Bottom ## Misc **o**: Open current q/a in the browser **y**: Copy current q/a to the clipboard **q, ZZ, Ctrl**: Exit **Ctrl**: Reload theme **?**: Toggle this help menu "###; Dialog::around( TextView::new(markdown::parse(bindings)) .scrollable() .with_name(NAME_HELP_VIEW), ) .dismiss_button("Close") .title("Help") } pub fn temp_feedback_msg(siv: &mut Cursive, msg: io::Result) { // TODO semaphore to close existing msg before displaying new one? let style = if msg.is_ok() { Color::Light(BaseColor::Green) } else { Color::Light(BaseColor::Red) }; let content = msg.unwrap_or_else(|e| format!("error: {e}")); let styled_content = SpannedString::styled(content, style); let layer = Dialog::around(TextView::new(styled_content)); let temp = TempView::new(layer, siv.cb_sink().clone()); siv.add_layer(temp); } // TODO see cursive/examples/src/bin/select_test.rs for how to test the interface! // maybe see if we can conditionally run when --nocapture is passed? so-0.4.10/src/tui/markdown.rs000064400000000000000000000574661046102023000141310ustar 00000000000000//! Parse markdown text. //! //! Extended from cursive::utils::markup::markdown to add code styles //! TODO: Bring in the full power of termimad (e.g. md tables) in a View; //! implementation of those features (.e.g automatic wrapping within each table //! cell) might be easier in this setting anyway. // TODO use ColorStyle::secondary() etc. over specific enums use cursive::theme::{Effect, PaletteColor, Style}; use cursive::utils::markup::{StyledIndexedSpan, StyledString}; use cursive::utils::span::{IndexedCow, IndexedSpan}; use pulldown_cmark::{self, CowStr, Event, HeadingLevel, Options, Tag}; pub type Markdown = StyledString; /// Parses the given string as markdown text. /// **Note**: Assumes preprocessing has taken place pub fn parse(input: S) -> StyledString where S: Into, { let input = input.into(); let spans = parse_spans(&input); //let output = build_output(&spans); StyledString::with_spans(input, spans) } pub fn preprocess(input: String) -> String { input .as_str() .trim() .replace("", "**[") .replace("", "]**") } /// Preview markdown of the given length /// Currently removes any color (i.e. code highlighting) to avoid /// the jarring issue of a fragmented highlight style on focused items. pub fn preview(width: usize, input: &StyledString) -> StyledString { let mut w = 0; let mut new_spans = Vec::new(); for span in input.spans_raw() { // Filter newlines if span.width == 0 { w += 1; new_spans.push(drop_color(IndexedSpan { content: IndexedCow::Owned(" ".to_owned()), width: 1, ..*span })); } else { w += span.width; new_spans.push(drop_color(span.clone())); } if w > width { break; } } let mut prev = StyledString::with_spans(input.source(), new_spans); prev.append_plain("..."); prev } fn drop_color(span: StyledIndexedSpan) -> StyledIndexedSpan { IndexedSpan { attr: Style { color: Default::default(), ..span.attr }, ..span } } /// Parse the given markdown text into a list of spans. /// This is a shortcut for `Parser::new(preprocessed_input).collect()`. fn parse_spans(input: &str) -> Vec { Parser::new(input).collect() } /// Iterator that parse a markdown text and outputs styled spans. pub struct Parser<'a, 'b> { first: bool, item: Option, in_list: bool, after_code_block: bool, stack: Vec