use std::io::{self, IsTerminal, Write}; use std::sync::Mutex; use std::time::Duration; use terminal_size::{terminal_size, Width}; pub const BANNER: [&str; 9] = [ " ░██████ ░██████ ", " ░██ ░██ ░██ ░██ ", "░██ ░██████ ░█████████████░██ ░███████ ░██████ ░████████ ░████████ ░███████ ░██░████ ", "░██ ░██░██ ░██ ░██░████████░██ ░██ ░██ ░██ ░██░██ ░██░██ ░██░███ ", "░██ ░███████░██ ░██ ░██ ░██░██ ░███████ ░██ ░██░██ ░██░█████████░██ ", " ░██ ░██░██ ░██░██ ░██ ░██░██ ░██░██ ░██░██ ░██░██ ░██░██ ░██░██ ░██ ", " ░██████ ░█████░█░██ ░██ ░██ ░██████ ░███████ ░█████░██░██ ░██░██ ░██ ░███████ ░██ ", " ", " ", ]; #[derive(Debug, Clone)] pub struct ScreenState { pub current_ip: String, pub tested: usize, pub total: usize, } pub struct TerminalUi { enabled: bool, lock: Mutex<()>, } #[derive(Debug, Clone)] pub struct RedisWorkerScreenState { pub worker_name: String, pub queue_name: String, pub current_ip: String, pub current_path: String, pub current_status: String, pub processed: usize, pub captured: usize, pub global_done: usize, pub global_total: usize, pub queue_remaining: Option, pub block_done: usize, pub block_total: usize, pub elapsed: Duration, } impl TerminalUi { pub fn new(enabled: bool) -> Self { Self { enabled, lock: Mutex::new(()), } } pub fn is_enabled(&self) -> bool { self.enabled } pub fn print_banner(&self) { if !self.enabled { return; } let _guard = self.lock.lock().expect("terminal mutex poisoned"); let mut stdout = io::stdout(); let _ = write!(stdout, "\x1b[2J\x1b[H\x1b[?25l"); for line in BANNER { let _ = writeln!(stdout, "{line}"); } let _ = writeln!(stdout, "IP: aguardando..."); let _ = writeln!( stdout, "Progress: [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] | 0.0%" ); let _ = stdout.flush(); } pub fn render_redis_worker(&self, state: &RedisWorkerScreenState) { if !self.enabled { return; } let _guard = self.lock.lock().expect("terminal mutex poisoned"); let mut stdout = io::stdout(); let width = terminal_width(); let content_width = width.saturating_sub(2).max(32); let global_percent = percent(state.global_done, state.global_total); let block_percent = percent(state.block_done, state.block_total); let global_bar = progress_bar(global_percent, content_width.saturating_sub(24)); let block_bar = progress_bar(block_percent, content_width.saturating_sub(24)); let queue_remaining = state .queue_remaining .map(|value| value.to_string()) .unwrap_or_else(|| "?".to_string()); let _ = write!(stdout, "\x1b[2J\x1b[H\x1b[?25l"); let _ = writeln!( stdout, "{}", fit_line( &format!( "CamFinder Redis Worker {} pid:{}", state.worker_name, std::process::id() ), content_width ) ); let _ = writeln!(stdout, "{}", "─".repeat(content_width)); let _ = writeln!( stdout, "{}", fit_line(&format!("Fila: {}", state.queue_name), content_width) ); let _ = writeln!( stdout, "{}", fit_line( &format!( "Global: [{}] {:>5.1}% {}/{} IPs redis:{}", global_bar, global_percent, state.global_done, state.global_total, queue_remaining ), content_width ) ); let _ = writeln!( stdout, "{}", fit_line( &format!( "Bloco : [{}] {:>5.1}% {}/{} tentativas", block_bar, block_percent, state.block_done, state.block_total ), content_width ) ); let _ = writeln!(stdout, "{}", "─".repeat(content_width)); let _ = writeln!( stdout, "{}", fit_line(&format!("IP atual : {}", state.current_ip), content_width) ); let _ = writeln!( stdout, "{}", fit_line(&format!("Path : {}", state.current_path), content_width) ); let _ = writeln!( stdout, "{}", fit_line( &format!("Status : {}", state.current_status), content_width ) ); let _ = writeln!( stdout, "{}", fit_line( &format!( "Tentativas: {} Capturas: {} Tempo: {}", state.processed, state.captured, format_duration(state.elapsed) ), content_width ) ); let _ = stdout.flush(); } pub fn render(&self, state: &ScreenState) { if !self.enabled { return; } let _guard = self.lock.lock().expect("terminal mutex poisoned"); let mut stdout = io::stdout(); let percent = if state.total == 0 { 100.0 } else { ((state.tested as f64 / state.total as f64) * 100.0).min(100.0) }; let bar_width = 36usize; let filled = ((percent / 100.0) * bar_width as f64).round() as usize; let empty = bar_width.saturating_sub(filled); let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty)); let ip = format!("{:<15}", state.current_ip); let percent = format!("{:>5.1}%", percent); let _ = write!( stdout, "\x1b[2F\x1b[2KIP: {ip}\n\x1b[2KProgress: [{bar}] | {percent}\n" ); let _ = stdout.flush(); } pub fn finish(&self) { if !self.enabled { return; } let _guard = self.lock.lock().expect("terminal mutex poisoned"); let mut stdout = io::stdout(); let _ = write!(stdout, "\x1b[?25h"); let _ = stdout.flush(); } } pub fn stdout_is_terminal() -> bool { io::stdout().is_terminal() } fn terminal_width() -> usize { terminal_size() .map(|(Width(width), _)| usize::from(width)) .unwrap_or(80) } fn percent(done: usize, total: usize) -> f64 { if total == 0 { 0.0 } else { ((done as f64 / total as f64) * 100.0).clamp(0.0, 100.0) } } fn progress_bar(percent: f64, width: usize) -> String { let width = width.clamp(8, 80); let filled = ((percent / 100.0) * width as f64).round() as usize; let empty = width.saturating_sub(filled); format!("{}{}", "█".repeat(filled), "░".repeat(empty)) } fn fit_line(value: &str, width: usize) -> String { let chars = value.chars().collect::>(); if chars.len() <= width { return value.to_string(); } if width <= 1 { return "…".to_string(); } chars .into_iter() .take(width - 1) .chain(std::iter::once('…')) .collect() } fn format_duration(duration: Duration) -> String { let total = duration.as_secs(); let hours = total / 3600; let minutes = (total % 3600) / 60; let seconds = total % 60; if hours > 0 { format!("{hours:02}:{minutes:02}:{seconds:02}") } else { format!("{minutes:02}:{seconds:02}") } }