Files
CamFinder/src/terminal.rs
T

265 lines
8.8 KiB
Rust

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<usize>,
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::<Vec<_>>();
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}")
}
}