feat: support exiting tui

This commit is contained in:
Almaz
2025-07-15 11:01:41 +00:00
committed by GitHub
parent 7225929f2f
commit d0796681ef
4 changed files with 192 additions and 102 deletions
Generated
+1
View File
@@ -1346,6 +1346,7 @@ dependencies = [
"serde",
"serde_json",
"tokio",
"tokio-util",
"toml",
"tracing",
"tracing-log",
+8 -1
View File
@@ -20,6 +20,7 @@ rlimit = "=0.10.2"
serde = "=1.0.219"
serde_json = "=1.0.140"
tokio = { version = "=1.46.1", features = ["full"] }
tokio-util = { version = "=0.7.15", optional = true }
toml = "=0.9.2"
tracing = "=0.1.41"
tracing-log = "=0.2.0"
@@ -30,7 +31,13 @@ tui-logger = { version = "=0.17.3", features = [
url = "=2.5.4"
[features]
tui = ["dep:crossterm", "dep:futures", "dep:ratatui", "dep:tui-logger"]
tui = [
"dep:crossterm",
"dep:futures",
"dep:ratatui",
"dep:tokio-util",
"dep:tui-logger",
]
[target.'cfg(not(target_os = "android"))'.dependencies]
reqwest = { version = "=0.12.22", default-features = false, features = [
+172 -75
View File
@@ -66,67 +66,45 @@ fn create_reqwest_client() -> reqwest::Result<reqwest::Client> {
.build()
}
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
color_eyre::install().wrap_err("failed to install color_eyre hooks")?;
fn create_logging_filter(
config: &config::Config,
) -> tracing_subscriber::filter::Targets {
let base = tracing_subscriber::filter::Targets::new()
.with_default(tracing::level_filters::LevelFilter::INFO)
.with_target(
"hickory_proto::udp::udp_client_stream",
tracing::level_filters::LevelFilter::ERROR,
)
.with_target(
// TODO: remove for hickory_proto >= 0.25.0
"hickory_proto::xfer::dns_exchange",
tracing::level_filters::LevelFilter::ERROR,
);
let raw_config_path = raw_config::get_config_path();
let raw_config = raw_config::read_config(std::path::Path::new(
&raw_config_path,
))
.await
.wrap_err_with(move || format!("failed to read {raw_config_path}"))?;
let config = Arc::new(
config::Config::from_raw_config(raw_config)
.await
.wrap_err("failed to create Config from RawConfig")?,
);
let targets_filter = {
let base = tracing_subscriber::filter::Targets::new()
.with_default(tracing::level_filters::LevelFilter::INFO)
.with_target(
"hickory_proto::udp::udp_client_stream",
tracing::level_filters::LevelFilter::ERROR,
)
.with_target(
// TODO: remove for hickory_proto >= 0.25.0
"hickory_proto::xfer::dns_exchange",
tracing::level_filters::LevelFilter::ERROR,
);
if config.debug {
base.with_target(
"proxy_scraper_checker::checker",
tracing::level_filters::LevelFilter::DEBUG,
)
} else {
base
}
};
#[cfg(feature = "tui")]
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
#[cfg(feature = "tui")]
let tui_task =
tokio::task::spawn(tui::Tui::new(targets_filter)?.run(tx.clone(), rx));
#[cfg(not(feature = "tui"))]
tracing_subscriber::registry()
.with(targets_filter)
.with(tracing_subscriber::fmt::layer())
.init();
let http_client = create_reqwest_client()
.wrap_err("failed to create reqwest HTTP client")?;
if config.debug {
base.with_target(
"proxy_scraper_checker::checker",
tracing::level_filters::LevelFilter::DEBUG,
)
} else {
base
}
}
fn spawn_ip_database_tasks(
config: &Arc<config::Config>,
http_client: &reqwest::Client,
#[cfg(feature = "tui")] tx: &tokio::sync::mpsc::UnboundedSender<
event::Event,
>,
) -> tokio::task::JoinSet<color_eyre::Result<()>> {
let mut output_dependencies_tasks = tokio::task::JoinSet::new();
if config.asn_enabled() {
let http_client = http_client.clone();
#[cfg(feature = "tui")]
let tx = tx.clone();
output_dependencies_tasks.spawn(async move {
ipdb::DbType::Asn
.download(
@@ -142,6 +120,7 @@ async fn main() -> color_eyre::Result<()> {
let http_client = http_client.clone();
#[cfg(feature = "tui")]
let tx = tx.clone();
output_dependencies_tasks.spawn(async move {
ipdb::DbType::Geo
.download(
@@ -153,6 +132,58 @@ async fn main() -> color_eyre::Result<()> {
});
}
output_dependencies_tasks
}
async fn wait_for_ip_database_tasks(
mut tasks: tokio::task::JoinSet<color_eyre::Result<()>>,
) -> color_eyre::Result<()> {
while let Some(task) = tasks.join_next().await {
task.wrap_err("output dependencies task panicked or was cancelled")??;
}
Ok(())
}
async fn process_proxies(
config: Arc<config::Config>,
proxies: std::collections::HashSet<crate::proxy::Proxy>,
#[cfg(feature = "tui")] tx: &tokio::sync::mpsc::UnboundedSender<
event::Event,
>,
) -> color_eyre::Result<Vec<crate::proxy::Proxy>> {
if config.checking.check_url.is_empty() {
Ok(proxies.into_iter().collect())
} else {
#[cfg(not(feature = "tui"))]
tracing::info!("Started checking {} proxies", proxies.len());
checker::check_all(
config,
proxies,
#[cfg(feature = "tui")]
tx.clone(),
)
.await
.wrap_err("failed to check proxies")
}
}
async fn main_task(
config: Arc<config::Config>,
#[cfg(feature = "tui")] tx: tokio::sync::mpsc::UnboundedSender<
event::Event,
>,
) -> color_eyre::Result<()> {
let http_client = create_reqwest_client()
.wrap_err("failed to create reqwest HTTP client")?;
let ip_db_tasks = spawn_ip_database_tasks(
&config,
&http_client,
#[cfg(feature = "tui")]
&tx,
);
let proxies = scraper::scrape_all(
Arc::clone(&config),
http_client.clone(),
@@ -164,26 +195,15 @@ async fn main() -> color_eyre::Result<()> {
drop(http_client);
while let Some(task) = output_dependencies_tasks.join_next().await {
task.wrap_err("output dependencies task panicked or was cancelled")??;
}
drop(output_dependencies_tasks);
wait_for_ip_database_tasks(ip_db_tasks).await?;
let proxies = if config.checking.check_url.is_empty() {
proxies.into_iter().collect()
} else {
#[cfg(not(feature = "tui"))]
tracing::info!("Started checking {} proxies", proxies.len());
checker::check_all(
Arc::clone(&config),
proxies,
#[cfg(feature = "tui")]
tx.clone(),
)
.await
.wrap_err("failed to check proxies")?
};
let proxies = process_proxies(
Arc::clone(&config),
proxies,
#[cfg(feature = "tui")]
&tx,
)
.await?;
output::save_proxies(config, proxies)
.await
@@ -194,11 +214,88 @@ async fn main() -> color_eyre::Result<()> {
#[cfg(feature = "tui")]
tx.send(event::Event::App(event::AppEvent::Done))?;
#[cfg(feature = "tui")]
drop(tx);
Ok(())
}
#[cfg(feature = "tui")]
async fn cancellable_main(
config: Arc<config::Config>,
tx: tokio::sync::mpsc::UnboundedSender<event::Event>,
token: tokio_util::sync::CancellationToken,
) -> color_eyre::Result<()> {
#[expect(clippy::integer_division_remainder_used)]
{
tokio::select! {
biased;
() = token.cancelled() => Ok(()),
r = main_task(config, tx) => r
}
}
}
#[cfg(feature = "tui")]
async fn run_with_tui(
config: Arc<config::Config>,
logging_filter: tracing_subscriber::filter::Targets,
) -> color_eyre::Result<()> {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let tui_task =
tokio::task::spawn(tui::Tui::new(logging_filter)?.run(tx.clone(), rx));
let token = tokio_util::sync::CancellationToken::new();
let main_task =
tokio::task::spawn(cancellable_main(config, tx, token.clone()));
#[cfg(feature = "tui")]
tui_task.await??;
token.cancel();
drop(token);
main_task.await??;
Ok(())
}
#[cfg(not(feature = "tui"))]
async fn run_without_tui(
config: Arc<config::Config>,
logging_filter: tracing_subscriber::filter::Targets,
) -> color_eyre::Result<()> {
tracing_subscriber::registry()
.with(logging_filter)
.with(tracing_subscriber::fmt::layer())
.init();
main_task(config).await
}
async fn load_config() -> color_eyre::Result<Arc<config::Config>> {
let raw_config_path = raw_config::get_config_path();
let raw_config = raw_config::read_config(std::path::Path::new(
&raw_config_path,
))
.await
.wrap_err_with(move || format!("failed to read {raw_config_path}"))?;
let config = config::Config::from_raw_config(raw_config)
.await
.wrap_err("failed to create Config from RawConfig")?;
Ok(Arc::new(config))
}
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
color_eyre::install().wrap_err("failed to install color_eyre hooks")?;
let config = load_config().await?;
let logging_filter = create_logging_filter(&config);
#[cfg(feature = "tui")]
{
run_with_tui(config, logging_filter).await
}
#[cfg(not(feature = "tui"))]
{
run_without_tui(config, logging_filter).await
}
}
+11 -26
View File
@@ -95,9 +95,6 @@ impl Drop for Tui {
pub enum AppMode {
#[default]
Running,
/// Wait for the user confirmation to close the UI
Done,
/// Close the UI
Quit,
}
@@ -301,17 +298,12 @@ fn draw(f: &mut Frame, state: &AppState, logger_state: &TuiWidgetState) {
working_proxies_block.inner(layout[2]),
);
}
let done = matches!(state.mode, AppMode::Done);
let mut lines = Vec::with_capacity(usize::from(done).saturating_add(2));
lines.push(Line::from("Up/PageUp/k - scroll logs up"));
lines.push(Line::from("Down/PageDown/j - scroll logs down"));
if done {
lines.push(
Line::from("Enter/ESC/q/Ctrl-C - exit")
.style(Style::default().fg(Color::Red)),
);
}
let lines = vec![
Line::from("Up/PageUp/k - scroll logs up"),
Line::from("Down/PageDown/j - scroll logs down"),
Line::from("ESC/q/Ctrl-C - exit")
.style(Style::default().fg(Color::Red)),
];
f.render_widget(Text::from(lines).centered(), outer_layout[3]);
}
@@ -329,16 +321,11 @@ async fn handle_event(
Event::Crossterm(crossterm_event) => {
match crossterm_event {
CrosstermEvent::Key(key_event) => match key_event.code {
KeyCode::Enter
| KeyCode::Esc
| KeyCode::Char('q' | 'Q')
if matches!(state.mode, AppMode::Done) =>
{
KeyCode::Esc | KeyCode::Char('q' | 'Q') => {
state.mode = AppMode::Quit;
}
KeyCode::Char('c' | 'C')
if key_event.modifiers == KeyModifiers::CONTROL
&& matches!(state.mode, AppMode::Done) =>
if key_event.modifiers == KeyModifiers::CONTROL =>
{
state.mode = AppMode::Quit;
}
@@ -407,11 +394,9 @@ async fn handle_event(
.or_insert(1);
}
AppEvent::Done => {
state.mode = if is_interactive().await {
AppMode::Done
} else {
AppMode::Quit
};
if !is_interactive().await {
state.mode = AppMode::Quit;
}
}
}
false