diff --git a/Cargo.lock b/Cargo.lock index ed9ea2c..7340ccd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,10 +226,10 @@ version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -362,7 +362,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -529,7 +529,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -644,6 +644,12 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -946,7 +952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -981,6 +987,12 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1026,6 +1038,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "memchr" version = "2.8.0" @@ -1127,7 +1145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1298,6 +1316,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "spinners", "tokio", "url", "urlencoding", @@ -1544,7 +1563,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1604,6 +1623,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spinners" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071af1a9d34b78b8db3ca4424b0ea4d87052d607dbe96287aebaccd596cabc86" +dependencies = [ + "lazy_static", + "maplit", + "strum", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1616,12 +1646,45 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[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.117" @@ -1650,7 +1713,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1713,7 +1776,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1724,7 +1787,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1786,7 +1849,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2022,7 +2085,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2398,7 +2461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -2409,10 +2472,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2428,7 +2491,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2495,7 +2558,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2516,7 +2579,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2536,7 +2599,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2576,7 +2639,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b41b776..da5aa00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ tokio = { version = "1.48.0", features = ["full"] } dnsclient = "0.2.0" futures = "0.3.32" clap_complete = "4.6.5" +spinners = "4.2.0" [profile.release] opt-level = "z" diff --git a/src/cmd/select.rs b/src/cmd/select.rs index 2218f13..6d63117 100644 --- a/src/cmd/select.rs +++ b/src/cmd/select.rs @@ -5,6 +5,7 @@ use std::time::Duration; use crate::entities::remote::Remote; use crate::entities::xray_server::{XRayServer, XRayServerWithDuration}; use crate::services::config::Config; +use crate::utils::spinner::SpinnerHandle; use crate::utils::tap::Tap; use crate::v2parser::entities::log::{Log, LogLevel}; use crate::v2parser::parser::create_json_config; @@ -37,6 +38,13 @@ pub(crate) struct SelectParams { #[arg(long, default_value_t = false, help = "Log DNS queries")] log_dns: bool, + + #[arg( + long = "skip-check", + default_value_t = false, + help = "Skip servers sorting by quick TCP check" + )] + skip_perf_check: bool, } impl SelectParams { @@ -103,6 +111,34 @@ impl SelectParams { Ok(item.url().to_string()) } + async fn get_servers<'a>(&self, xray_servers: &'a [XRayServer]) -> Vec { + if self.skip_perf_check { + return xray_servers + .iter() + .map(|s| XRayServerWithDuration::new(s.clone(), None)) + .collect(); + } + let spinner = SpinnerHandle::new("Sorting servers".to_string()); + let sorted = self.sort_servers(xray_servers).await; + spinner.stop("Done sorting servers".to_string()); + + sorted + } + + fn count_stats<'a>(servers: &'a [XRayServerWithDuration]) -> (usize, usize) { + let mut alive = 0; + let mut dead = 0; + + for server in servers { + match server.is_dead() { + true => dead += 1, + false => alive += 1, + } + } + + (alive, dead) + } + async fn select_remote(&self) -> anyhow::Result { let config = Config::read_or_default().await; let remotes: Vec = config @@ -128,6 +164,7 @@ impl SelectParams { .get(remote_idx) .ok_or_else(|| anyhow::Error::msg("No remote URL"))?; + let spinner = SpinnerHandle::new("Loading servers".to_string()); let content = Client::builder() .build()? .get(selected_remote.url()) @@ -142,15 +179,26 @@ impl SelectParams { .iter() .filter_map(|u| XRayServer::try_from(u.to_string()).ok()) .collect::>(); - - let sorted = self.sort_servers(xray_servers.as_slice()).await; + spinner.stop("Done loading servers".to_string()); if xray_servers.is_empty() { return Err(anyhow::Error::msg("This URL has note XRay servers")); } + let sorted = self.get_servers(xray_servers.as_slice()).await; + + let select_message = match self.skip_perf_check { + true => "Select one XRay server".to_string(), + false => { + let (accessible, dead) = Self::count_stats(sorted.as_slice()); + format!( + "Select one XRay server ({accessible} accessible, {dead} considered dead due to TCP check)" + ) + } + }; + let server_idx = dialoguer::FuzzySelect::new() - .with_prompt("Select one XRay server") + .with_prompt(select_message.as_str()) .items(&sorted) .tap(|d| match &xray_servers.is_empty() { true => d, diff --git a/src/entities/xray_server.rs b/src/entities/xray_server.rs index 74b27df..8987836 100644 --- a/src/entities/xray_server.rs +++ b/src/entities/xray_server.rs @@ -11,6 +11,10 @@ use url::{Host, Url}; const UNNAMED: &'static str = "Unnamed XRay Server"; +/// Maximum round‑trip time (in milliseconds) allowed before +/// a server is considered dead. +const THRESHOLD_MS: u128 = 10_000; + #[derive(Clone, Debug, Serialize, Deserialize, PartialOrd, PartialEq, Eq, Ord, Hash)] #[serde(transparent)] pub(crate) struct XRayServer(Url); @@ -114,6 +118,13 @@ impl XRayServerWithDuration { duration, } } + + pub fn is_dead(&self) -> bool { + match self.duration { + Some(d) => d.as_millis() > THRESHOLD_MS, + None => true, + } + } } impl Display for XRayServerWithDuration { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index aa8c44b..3289976 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,3 @@ +pub(crate) mod spinner; pub(crate) mod tap; pub(crate) mod to_err; diff --git a/src/utils/spinner.rs b/src/utils/spinner.rs new file mode 100644 index 0000000..46588ec --- /dev/null +++ b/src/utils/spinner.rs @@ -0,0 +1,14 @@ +use spinners::{Spinner, Spinners}; + +pub(crate) struct SpinnerHandle(Spinner); + +impl SpinnerHandle { + pub fn new(message: String) -> Self { + let spinner = Spinner::new(Spinners::Dots9, message); + Self(spinner) + } + + pub fn stop(mut self, msg: String) { + self.0.stop_with_message(msg); + } +}