From 2ee9ebd0e33d0de26afafbe468ed32b75a9fdc4a Mon Sep 17 00:00:00 2001 From: monosans Date: Tue, 6 Jan 2026 22:54:02 +0300 Subject: [PATCH] refactor tui code --- ratatui | 1 + src/output.rs | 2 +- src/proxy.rs | 14 ++- src/tui.rs | 247 +++++++++++++++++++++++++++++--------------------- 4 files changed, 157 insertions(+), 107 deletions(-) create mode 160000 ratatui diff --git a/ratatui b/ratatui new file mode 160000 index 0000000..0a2a7c0 --- /dev/null +++ b/ratatui @@ -0,0 +1 @@ +Subproject commit 0a2a7c0363a4806b0cf05c1915bf7cdd438f756c diff --git a/src/output.rs b/src/output.rs index 24d92f8..d528c9a 100644 --- a/src/output.rs +++ b/src/output.rs @@ -177,7 +177,7 @@ pub async fn save_proxies( .await?; for (proto, proxies) in grouped_proxies { - let mut file_path = directory_path.join(proto.as_str()); + let mut file_path = directory_path.join(proto.as_str_lowercase()); file_path.set_extension("txt"); write_proxy_list_to_file(&file_path, proxies, false).await?; } diff --git a/src/proxy.rs b/src/proxy.rs index 3b1a2c1..4db70d1 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -40,13 +40,21 @@ impl FromStr for ProxyType { } impl ProxyType { - pub const fn as_str(self) -> &'static str { + pub const fn as_str_lowercase(self) -> &'static str { match self { Self::Http => "http", Self::Socks4 => "socks4", Self::Socks5 => "socks5", } } + + pub const fn as_str_uppercase(self) -> &'static str { + match self { + Self::Http => "HTTP", + Self::Socks4 => "SOCKS4", + Self::Socks5 => "SOCKS5", + } + } } #[derive(Eq)] @@ -68,7 +76,7 @@ impl TryFrom<&mut Proxy> for reqwest::Proxy { let proxy = Self::all( compact_str::format_compact!( "{}://{}:{}", - value.protocol.as_str(), + value.protocol.as_str_lowercase(), value.host, value.port ) @@ -165,7 +173,7 @@ impl Proxy { include_protocol: bool, ) { if include_protocol { - sink.push_str(self.protocol.as_str()); + sink.push_str(self.protocol.as_str_lowercase()); sink.push_str("://"); } diff --git a/src/tui.rs b/src/tui.rs index 22135b4..035e722 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -28,6 +28,7 @@ use crate::{ }; const FPS: f64 = 30.0; +const TICK_RATE: f64 = 1.0 / FPS; pub struct RatatuiRestoreGuard; impl Drop for RatatuiRestoreGuard { @@ -97,7 +98,7 @@ pub struct AppState { } async fn tick_event_listener(tx: tokio::sync::mpsc::UnboundedSender) { - let mut tick = tokio::time::interval(Duration::from_secs_f64(1.0 / FPS)); + let mut tick = tokio::time::interval(Duration::from_secs_f64(TICK_RATE)); loop { tokio::select! { biased; @@ -141,13 +142,18 @@ async fn crossterm_event_listener( } fn draw(f: &mut Frame<'_>, state: &AppState, logger_state: &TuiWidgetState) { - let outer_block = Block::default() - .title("https://github.com/monosans/proxy-scraper-checker") + let outer_block = Block::new() + .title(Line { + spans: vec![ + "https://github.com/monosans/proxy-scraper-checker".into(), + ], + ..Default::default() + }) .title_alignment(Alignment::Center); f.render_widget(outer_block.clone(), f.area()); - let outer_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ + let outer_layout = Layout::new( + Direction::Vertical, + [ // Logs Constraint::Fill(1), // IP database download @@ -156,51 +162,54 @@ fn draw(f: &mut Frame<'_>, state: &AppState, logger_state: &TuiWidgetState) { Constraint::Length(1 + (3 * 3) + 1), // Hotkeys Constraint::Length(4), - ]) - .split(outer_block.inner(f.area())); + ], + ) + .split(outer_block.inner(f.area())); drop(outer_block); f.render_widget( TuiLoggerWidget::default() - .state(logger_state) - .block(Block::bordered().title("Logs")) + .block(Block::bordered().title(Line { + spans: vec!["Logs".into()], + ..Default::default() + })) + .style_error(Style::new().fg(Color::Red)) + .style_warn(Style::new().fg(Color::Yellow)) + .style_info(Style::new().fg(Color::Cyan)) + .style_trace(Style::new().fg(Color::Magenta)) + .style_debug(Style::new().fg(Color::Green)) .output_file(false) .output_line(false) - .style_trace(Style::default().fg(Color::Magenta)) - .style_debug(Style::default().fg(Color::Green)) - .style_info(Style::default().fg(Color::Cyan)) - .style_warn(Style::default().fg(Color::Yellow)) - .style_error(Style::default().fg(Color::Red)), + .state(logger_state), outer_layout[0], ); - let ipdb_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Fill(1); 2]) - .split(outer_layout[1]); + let ipdb_layout = + Layout::new(Direction::Horizontal, [Constraint::Fill(1); 2]) + .split(outer_layout[1]); f.render_widget( Gauge::default() - .block(Block::bordered().title("ASN database download")) - .ratio({ - if state.asn_db_total == 0 { - 1.0 - } else { - (state.asn_db_downloaded as f64) - / (state.asn_db_total as f64) - } + .block(Block::bordered().title(Line { + spans: vec!["ASN database download".into()], + ..Default::default() + })) + .ratio(if state.asn_db_total == 0 { + 1.0 + } else { + (state.asn_db_downloaded as f64) / (state.asn_db_total as f64) }), ipdb_layout[0], ); f.render_widget( Gauge::default() - .block(Block::bordered().title("Geolocation database download")) - .ratio({ - if state.geo_db_total == 0 { - 1.0 - } else { - (state.geo_db_downloaded as f64) - / (state.geo_db_total as f64) - } + .block(Block::bordered().title(Line { + spans: vec!["Geolocation database download".into()], + ..Default::default() + })) + .ratio(if state.geo_db_total == 0 { + 1.0 + } else { + (state.geo_db_downloaded as f64) / (state.geo_db_total as f64) }), ipdb_layout[1], ); @@ -210,18 +219,20 @@ fn draw(f: &mut Frame<'_>, state: &AppState, logger_state: &TuiWidgetState) { state.sources_total.keys().collect(); proxy_types.sort_unstable(); - let proxies_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints(proxy_types.iter().map(|_| Constraint::Fill(1))) - .split(outer_layout[2]); + let proxies_layout = Layout::new( + Direction::Horizontal, + std::iter::repeat_n(Constraint::Fill(1), proxy_types.len()), + ) + .split(outer_layout[2]); for (i, proxy_type) in proxy_types.into_iter().enumerate() { - let block = Block::bordered().title(proxy_type.as_str().to_uppercase()); + let block = Block::bordered().title(Line { + spans: vec![proxy_type.as_str_uppercase().into()], + ..Default::default() + }); f.render_widget(block.clone(), proxies_layout[i]); - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Fill(1); 3]) + let layout = Layout::new(Direction::Vertical, [Constraint::Fill(1); 3]) .split(block.inner(proxies_layout[i])); drop(block); @@ -232,14 +243,15 @@ fn draw(f: &mut Frame<'_>, state: &AppState, logger_state: &TuiWidgetState) { f.render_widget( Gauge::default() - .ratio({ - if sources_total == 0 { - 1.0 - } else { - (sources_scraped as f64) / (sources_total as f64) - } + .block(Block::bordered().title(Line { + spans: vec!["Scraping sources".into()], + ..Default::default() + })) + .ratio(if sources_total == 0 { + 1.0 + } else { + (sources_scraped as f64) / (sources_total as f64) }) - .block(Block::bordered().title("Scraping sources")) .label(compact_str::format_compact!( "{sources_scraped}/{sources_total}" )), @@ -252,38 +264,47 @@ fn draw(f: &mut Frame<'_>, state: &AppState, logger_state: &TuiWidgetState) { state.proxies_checked.get(proxy_type).copied().unwrap_or_default(); f.render_widget( Gauge::default() - .ratio({ - if proxies_total == 0 { - 1.0 - } else { - (proxies_checked as f64) / (proxies_total as f64) - } + .block(Block::bordered().title(Line { + spans: vec!["Checking proxies".into()], + ..Default::default() + })) + .ratio(if proxies_total == 0 { + 1.0 + } else { + (proxies_checked as f64) / (proxies_total as f64) }) - .block(Block::bordered().title("Checking proxies")) .label(compact_str::format_compact!( "{proxies_checked}/{proxies_total}" )), layout[1], ); - let working_proxies_block = Block::bordered().title("Working proxies"); + let working_proxies_block = Block::bordered().title(Line { + spans: vec!["Working proxies".into()], + ..Default::default() + }); f.render_widget(working_proxies_block.clone(), layout[2]); let proxies_working = state.proxies_working.get(proxy_type).copied().unwrap_or_default(); f.render_widget( - Line::from( - compact_str::format_compact!("{} ({:.1}%)", proxies_working, { - if proxies_checked == 0 { - 0.0_f64 - } else { - (proxies_working as f64) / (proxies_checked as f64) - * 100.0_f64 - } - }) - .as_str(), - ) - .alignment(Alignment::Center), + Line { + alignment: Some(Alignment::Center), + spans: vec![ + compact_str::format_compact!( + "{} ({:.1}%)", + proxies_working, + if proxies_checked == 0 { + 0.0_f64 + } else { + (proxies_working as f64) / (proxies_checked as f64) + * 100.0_f64 + } + ) + .into(), + ], + ..Default::default() + }, working_proxies_block.inner(layout[2]), ); } @@ -292,24 +313,38 @@ fn draw(f: &mut Frame<'_>, state: &AppState, logger_state: &TuiWidgetState) { let running = matches!(state.mode, AppMode::Running); let mut lines = Vec::with_capacity(if running { 4 } else { 3 }); - lines.push(Line::from("Up / PageUp / k - scroll logs up")); - lines.push(Line::from("Down / PageDown / j - scroll logs down")); + lines.push(Line { + spans: vec!["Up / PageUp / k - scroll logs up".into()], + ..Default::default() + }); + lines.push(Line { + spans: vec!["Down / PageDown / j - scroll logs down".into()], + ..Default::default() + }); if running { - lines.push( - Line::from("ESC / q - stop") - .style(Style::default().fg(Color::Yellow)), - ); + lines.push(Line { + style: Style::new().fg(Color::Yellow), + spans: vec!["ESC / q - stop".into()], + ..Default::default() + }); } - lines.push( - Line::from(if running { - "Ctrl-C - quit" - } else { - "ESC / q / Ctrl-C - quit" - }) - .style(Style::default().fg(Color::Red)), - ); + lines.push(Line { + style: Style::new().fg(Color::Red), + spans: vec![ + if running { "Ctrl-C - quit" } else { "ESC / q / Ctrl-C - quit" } + .into(), + ], + ..Default::default() + }); - f.render_widget(Text::from(lines).centered(), outer_layout[3]); + f.render_widget( + Text { + alignment: Some(Alignment::Center), + lines, + ..Default::default() + }, + outer_layout[3], + ); } fn handle_event( @@ -322,25 +357,31 @@ fn handle_event( Event::Tick => true, Event::Crossterm(crossterm_event) => { match crossterm_event { - CrosstermEvent::Key(key_event) => match key_event.code { - KeyCode::Esc | KeyCode::Char('q' | 'Q') => { - state.mode = state.mode.next(); - token.cancel(); + CrosstermEvent::Key(key_event) if key_event.is_press() => { + match key_event.code { + KeyCode::Esc | KeyCode::Char('q' | 'Q') => { + state.mode = state.mode.next(); + token.cancel(); + } + KeyCode::Char('c' | 'C') + if key_event.modifiers == KeyModifiers::CONTROL => + { + state.mode = AppMode::Quit; + token.cancel(); + } + KeyCode::Up | KeyCode::PageUp | KeyCode::Char('k') => { + logger_state + .transition(TuiWidgetEvent::PrevPageKey); + } + KeyCode::Down + | KeyCode::PageDown + | KeyCode::Char('j') => { + logger_state + .transition(TuiWidgetEvent::NextPageKey); + } + _ => {} } - KeyCode::Char('c' | 'C') - if key_event.modifiers == KeyModifiers::CONTROL => - { - state.mode = AppMode::Quit; - token.cancel(); - } - KeyCode::Up | KeyCode::PageUp | KeyCode::Char('k') => { - logger_state.transition(TuiWidgetEvent::PrevPageKey); - } - KeyCode::Down | KeyCode::PageDown | KeyCode::Char('j') => { - logger_state.transition(TuiWidgetEvent::NextPageKey); - } - _ => {} - }, + } CrosstermEvent::Mouse(mouse_event) => match mouse_event.kind { MouseEventKind::ScrollUp => { logger_state.transition(TuiWidgetEvent::PrevPageKey);